From 9d71a83412266bfe5095ac1fd1404b8adc5dadf6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 5 Sep 2024 11:04:44 +0200 Subject: [PATCH 001/122] just the template --- .../migrations/000249_email_reports.down.sql | 1 + .../migrations/000249_email_reports.up.sql | 20 ++++++++ coderd/notifications/enqueuer.go | 7 ++- coderd/notifications/events.go | 2 + coderd/notifications/notifications_test.go | 50 +++++++++++++++++++ ...mplateWorkspaceBuildSummary-body.md.golden | 17 +++++++ ...plateWorkspaceBuildSummary-title.md.golden | 1 + coderd/notifications/types/payload.go | 1 + 8 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 coderd/database/migrations/000249_email_reports.down.sql create mode 100644 coderd/database/migrations/000249_email_reports.up.sql create mode 100644 coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden create mode 100644 coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-title.md.golden diff --git a/coderd/database/migrations/000249_email_reports.down.sql b/coderd/database/migrations/000249_email_reports.down.sql new file mode 100644 index 0000000000000..ade1beee5a558 --- /dev/null +++ b/coderd/database/migrations/000249_email_reports.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00'; diff --git a/coderd/database/migrations/000249_email_reports.up.sql b/coderd/database/migrations/000249_email_reports.up.sql new file mode 100644 index 0000000000000..7934e2c1cd1ca --- /dev/null +++ b/coderd/database/migrations/000249_email_reports.up.sql @@ -0,0 +1,20 @@ +INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) +VALUES ('34a20db2-e9cc-4a93-b0e4-8569699d7a00', 'Report: Workspace Builds Failed For Template', E'Workspace builds failed for template "{{.Labels.template_display_name}}"', + E'Hi {{.UserName}}, + +Template **{{.Labels.template_display_name}}** has failed to build {{.Data.failed_builds}}/{{.Data.total_builds}} times over the last {{.Data.report_frequency}} and may be unstable. + +**Report:** +{{range $version := .Data.template_versions}} + **{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1}}s{{end}}: + {{range $build := $version.failed_builds}} + * [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}}) + {{- end}} +{{end}} +We recommend reviewing these issues to ensure future builds are successful.', + 'Template Events', '[ + { + "label": "View workspaces", + "url": "{{ base_url }}/workspaces?filter=template%3A{{.Labels.template_name}}" + } + ]'::jsonb); diff --git a/coderd/notifications/enqueuer.go b/coderd/notifications/enqueuer.go index 2915299ef26d5..743910b4c030e 100644 --- a/coderd/notifications/enqueuer.go +++ b/coderd/notifications/enqueuer.go @@ -69,7 +69,8 @@ func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUI dispatchMethod = metadata.CustomMethod.NotificationMethod } - payload, err := s.buildPayload(metadata, labels) + data := map[string]any{} // FIXME + payload, err := s.buildPayload(metadata, labels, data) if err != nil { s.log.Warn(ctx, "failed to build payload", slog.F("template_id", templateID), slog.F("user_id", userID), slog.Error(err)) return nil, xerrors.Errorf("enqueue notification (payload build): %w", err) @@ -119,7 +120,7 @@ func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUI // buildPayload creates the payload that the notification will for variable substitution and/or routing. // The payload contains information about the recipient, the event that triggered the notification, and any subsequent // actions which can be taken by the recipient. -func (s *StoreEnqueuer) buildPayload(metadata database.FetchNewMessageMetadataRow, labels map[string]string) (*types.MessagePayload, error) { +func (s *StoreEnqueuer) buildPayload(metadata database.FetchNewMessageMetadataRow, labels map[string]string, data map[string]any) (*types.MessagePayload, error) { payload := types.MessagePayload{ Version: "1.0", @@ -131,6 +132,8 @@ func (s *StoreEnqueuer) buildPayload(metadata database.FetchNewMessageMetadataRo UserUsername: metadata.UserUsername, Labels: labels, + Data: data, + // No actions yet } diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 6ba88c239edc8..12e4a9026d68b 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -29,4 +29,6 @@ var ( // Template-related events. var ( TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be") + + TemplateWorkspaceBuildSummary = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") ) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 567866a0aaf35..00369382e2c26 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -844,6 +844,56 @@ func TestNotificationTemplatesCanRender(t *testing.T) { }, }, }, + { + name: "TemplateWorkspaceBuildSummary", + id: notifications.TemplateWorkspaceBuildSummary, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "template_name": "bobby-first-template", + "template_display_name": "Bobby First Template", + }, + Data: map[string]any{ + "failed_builds": 4, + "total_builds": 55, + "report_frequency": "week", + "template_versions": []map[string]any{ + { + "template_version_name": "bobby-template-version-1", + "failed_count": 3, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": "mtojek", + "workspace_name": "workspace-1", + "build_number": 1234, + }, + { + "workspace_owner_username": "johndoe", + "workspace_name": "my-workspace-3", + "build_number": 5678, + }, + { + "workspace_owner_username": "jack", + "workspace_name": "workwork", + "build_number": 774, + }, + }, + }, + { + "template_version_name": "bobby-template-version-2", + "failed_count": 1, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": "ben", + "workspace_name": "cool-workspace", + "build_number": 8888, + }, + }, + }, + }, + }, + }, + }, } allTemplates, err := enumerateAllTemplates(t) diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden new file mode 100644 index 0000000000000..1bf726b24927f --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden @@ -0,0 +1,17 @@ +Hi Bobby, + +Template **Bobby First Template** has failed to build 4/55 times over the last week and may be unstable. + +**Report:** + + **bobby-template-version-1** failed 3 times: + + * [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234) + * [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678) + * [jack / workwork / #774](http://test.com/@jack/workwork/builds/774) + + **bobby-template-version-2** failed 1 time: + + * [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888) + +We recommend reviewing these issues to ensure future builds are successful. diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-title.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-title.md.golden new file mode 100644 index 0000000000000..f03f8fca96c7c --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-title.md.golden @@ -0,0 +1 @@ +Workspace builds failed for template "Bobby First Template" \ No newline at end of file diff --git a/coderd/notifications/types/payload.go b/coderd/notifications/types/payload.go index ba666219af654..5ef67be6ada6f 100644 --- a/coderd/notifications/types/payload.go +++ b/coderd/notifications/types/payload.go @@ -16,4 +16,5 @@ type MessagePayload struct { Actions []TemplateAction `json:"actions"` Labels map[string]string `json:"labels"` + Data map[string]any `json:"data,omitempty"` } From 7fc0b02557d650dd03e7636036d15bbe09be268c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 5 Sep 2024 14:03:56 +0200 Subject: [PATCH 002/122] Bobby --- coderd/notifications/notifications_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 00369382e2c26..781baafee0a16 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -848,7 +848,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) { name: "TemplateWorkspaceBuildSummary", id: notifications.TemplateWorkspaceBuildSummary, payload: types.MessagePayload{ - UserName: "bobby", + UserName: "Bobby", Labels: map[string]string{ "template_name": "bobby-first-template", "template_display_name": "Bobby First Template", From 1ff2a24cc820aea4dd8137488cc862bb7833585b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 5 Sep 2024 12:11:55 +0000 Subject: [PATCH 003/122] golden --- .../TemplateWorkspaceBuildSummary-body.md.golden | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden index 1bf726b24927f..d5b21eb06d042 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden @@ -5,13 +5,13 @@ Template **Bobby First Template** has failed to build 4/55 times over the last w **Report:** **bobby-template-version-1** failed 3 times: - + * [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234) * [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678) * [jack / workwork / #774](http://test.com/@jack/workwork/builds/774) **bobby-template-version-2** failed 1 time: - + * [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888) -We recommend reviewing these issues to ensure future builds are successful. +We recommend reviewing these issues to ensure future builds are successful. \ No newline at end of file From 15ede2140f477869e83c2315375bd4b2bd5ade00 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 5 Sep 2024 14:11:57 +0000 Subject: [PATCH 004/122] WIP --- .../migrations/000249_email_reports.up.sql | 8 ++++---- coderd/notifications/notifications_test.go | 2 +- .../TemplateWorkspaceBuildSummary-body.md.golden | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/coderd/database/migrations/000249_email_reports.up.sql b/coderd/database/migrations/000249_email_reports.up.sql index 7934e2c1cd1ca..0e65fe7dd2337 100644 --- a/coderd/database/migrations/000249_email_reports.up.sql +++ b/coderd/database/migrations/000249_email_reports.up.sql @@ -6,10 +6,10 @@ Template **{{.Labels.template_display_name}}** has failed to build {{.Data.faile **Report:** {{range $version := .Data.template_versions}} - **{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1}}s{{end}}: - {{range $build := $version.failed_builds}} - * [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}}) - {{- end}} +**{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1}}s{{end}}: +{{range $build := $version.failed_builds}} +* [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}}) +{{- end}} {{end}} We recommend reviewing these issues to ensure future builds are successful.', 'Template Events', '[ diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 781baafee0a16..8a018805e6239 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -51,7 +51,7 @@ import ( ) // updateGoldenFiles is a flag that can be set to update golden files. -var updateGoldenFiles = flag.Bool("update", false, "Update golden files") +var updateGoldenFiles = flag.Bool("update", true, "Update golden files") func TestMain(m *testing.M) { goleak.VerifyTestMain(m) diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden index d5b21eb06d042..cbd9b2e690c60 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden @@ -4,14 +4,14 @@ Template **Bobby First Template** has failed to build 4/55 times over the last w **Report:** - **bobby-template-version-1** failed 3 times: - - * [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234) - * [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678) - * [jack / workwork / #774](http://test.com/@jack/workwork/builds/774) +**bobby-template-version-1** failed 3 times: - **bobby-template-version-2** failed 1 time: - - * [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888) +* [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234) +* [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678) +* [jack / workwork / #774](http://test.com/@jack/workwork/builds/774) + +**bobby-template-version-2** failed 1 time: + +* [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888) We recommend reviewing these issues to ensure future builds are successful. \ No newline at end of file From 380c81de27ec809eb452ba5ba329a926d4be3309 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 10:34:34 +0200 Subject: [PATCH 005/122] generator --- coderd/notifications/reports/generator.go | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 coderd/notifications/reports/generator.go diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go new file mode 100644 index 0000000000000..df9f0d921b324 --- /dev/null +++ b/coderd/notifications/reports/generator.go @@ -0,0 +1,62 @@ +package reports + +import ( + "context" + "io" + "log/slog" + "time" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/quartz" +) + +const ( + delay = 5 * time.Minute +) + +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.Clock) io.Closer { + closed := make(chan struct{}) + + ctx, cancelFunc := context.WithCancel(ctx) + //nolint:gocritic // The system generates periodic reports without direct user input. + ctx = dbauthz.AsSystemRestricted(ctx) + + // Start the ticker with the initial delay. + ticker := clk.NewTicker(delay) + doTick := func(start time.Time) { + defer ticker.Reset(delay) + } + + go func() { + defer close(closed) + defer ticker.Stop() + // Force an initial tick. + doTick(dbtime.Time(clk.Now()).UTC()) + for { + select { + case <-ctx.Done(): + return + case tick := <-ticker.C: + ticker.Stop() + doTick(dbtime.Time(tick).UTC()) + } + } + }() + return &reportGenerator{ + cancel: cancelFunc, + closed: closed, + } +} + +type reportGenerator struct { + cancel context.CancelFunc + closed chan struct{} +} + +func (i *reportGenerator) Close() error { + i.cancel() + <-i.closed + return nil +} From 8aa1d9ad6d0ca9aa37ea7a2507a60ae10a5b5472 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 11:44:26 +0200 Subject: [PATCH 006/122] lock --- coderd/database/lock.go | 1 + coderd/notifications/reports/generator.go | 24 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/coderd/database/lock.go b/coderd/database/lock.go index b724e9b26dbd9..3e54577db59db 100644 --- a/coderd/database/lock.go +++ b/coderd/database/lock.go @@ -10,6 +10,7 @@ const ( LockIDEnterpriseDeploymentSetup LockIDDBRollup LockIDDBPurge + LockIDReportGenerator ) // GenLockID generates a unique and consistent lock ID from a given string. diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index df9f0d921b324..45a4b7b0bcb29 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -3,9 +3,10 @@ package reports import ( "context" "io" - "log/slog" "time" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -27,6 +28,27 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto ticker := clk.NewTicker(delay) doTick := func(start time.Time) { defer ticker.Reset(delay) + // Start a transaction to grab advisory lock, we don't want to run generator jobs at the same time (multiple replicas). + if err := db.InTx(func(tx database.Store) error { + // Acquire a lock to ensure that only one instance of the generator is running at a time. + ok, err := tx.TryAcquireLock(ctx, database.LockIDReportGenerator) + if err != nil { + return err + } + if !ok { + logger.Debug(ctx, "unable to acquire lock for generating periodic reports, skipping") + return nil + } + + // TODO + + logger.Info(ctx, "report generator finished", slog.F("duration", clk.Since(start))) + + return nil + }, nil); err != nil { + logger.Error(ctx, "failed to generate reports", slog.Error(err)) + return + } } go func() { From fcd6d61b4674badb6c0632c945866bd71e8342d2 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 11:53:10 +0200 Subject: [PATCH 007/122] notif data --- coderd/autobuild/lifecycle_executor.go | 3 ++- coderd/notifications/enqueuer.go | 8 ++++++-- coderd/notifications/spec.go | 1 + testutil/notifications.go | 8 +++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 5bd8efe2b9fcf..ed63c10ad341e 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -303,7 +303,8 @@ func (e *Executor) runOnce(t time.Time) Stats { "reason": nextBuildReason, "template_version_name": activeTemplateVersion.Name, "template_version_message": activeTemplateVersion.Message, - }, "autobuild", + }, + "autobuild", // Associate this notification with all the related entities. ws.ID, ws.OwnerID, ws.TemplateID, ws.OrganizationID, ); err != nil { diff --git a/coderd/notifications/enqueuer.go b/coderd/notifications/enqueuer.go index 743910b4c030e..86642cb1b010e 100644 --- a/coderd/notifications/enqueuer.go +++ b/coderd/notifications/enqueuer.go @@ -52,9 +52,14 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem }, nil } +// Enqueue queues a notification message for later delivery, assumes no structured input data. +func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { + return s.EnqueueData(ctx, userID, templateID, labels, map[string]any{}, createdBy, targets...) +} + // Enqueue queues a notification message for later delivery. // Messages will be dequeued by a notifier later and dispatched. -func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { +func (s *StoreEnqueuer) EnqueueData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { metadata, err := s.store.FetchNewMessageMetadata(ctx, database.FetchNewMessageMetadataParams{ UserID: userID, NotificationTemplateID: templateID, @@ -69,7 +74,6 @@ func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUI dispatchMethod = metadata.CustomMethod.NotificationMethod } - data := map[string]any{} // FIXME payload, err := s.buildPayload(metadata, labels, data) if err != nil { s.log.Warn(ctx, "failed to build payload", slog.F("template_id", templateID), slog.F("user_id", userID), slog.Error(err)) diff --git a/coderd/notifications/spec.go b/coderd/notifications/spec.go index c41189ba3d582..a078bfdc21242 100644 --- a/coderd/notifications/spec.go +++ b/coderd/notifications/spec.go @@ -33,4 +33,5 @@ type Handler interface { // Enqueuer enqueues a new notification message in the store and returns its ID, should it enqueue without failure. type Enqueuer interface { Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) + EnqueueData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) } diff --git a/testutil/notifications.go b/testutil/notifications.go index a8d6486209d2a..df9983342a6a3 100644 --- a/testutil/notifications.go +++ b/testutil/notifications.go @@ -15,11 +15,16 @@ type FakeNotificationsEnqueuer struct { type Notification struct { UserID, TemplateID uuid.UUID Labels map[string]string + Data map[string]any CreatedBy string Targets []uuid.UUID } -func (f *FakeNotificationsEnqueuer) Enqueue(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { +func (f *FakeNotificationsEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { + return f.EnqueueData(ctx, userID, templateID, labels, map[string]any{}, createdBy, targets...) +} + +func (f *FakeNotificationsEnqueuer) EnqueueData(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { f.mu.Lock() defer f.mu.Unlock() @@ -27,6 +32,7 @@ func (f *FakeNotificationsEnqueuer) Enqueue(_ context.Context, userID, templateI UserID: userID, TemplateID: templateID, Labels: labels, + Data: data, CreatedBy: createdBy, Targets: targets, }) From 1a941f826227529788769bce05fabbbbf0c2042b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 11:54:02 +0200 Subject: [PATCH 008/122] fix --- coderd/autobuild/lifecycle_executor.go | 3 +-- coderd/notifications/notifications_test.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index ed63c10ad341e..5bd8efe2b9fcf 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -303,8 +303,7 @@ func (e *Executor) runOnce(t time.Time) Stats { "reason": nextBuildReason, "template_version_name": activeTemplateVersion.Name, "template_version_message": activeTemplateVersion.Message, - }, - "autobuild", + }, "autobuild", // Associate this notification with all the related entities. ws.ID, ws.OwnerID, ws.TemplateID, ws.OrganizationID, ); err != nil { diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 8a018805e6239..781baafee0a16 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -51,7 +51,7 @@ import ( ) // updateGoldenFiles is a flag that can be set to update golden files. -var updateGoldenFiles = flag.Bool("update", true, "Update golden files") +var updateGoldenFiles = flag.Bool("update", false, "Update golden files") func TestMain(m *testing.M) { goleak.VerifyTestMain(m) From be24cbdc64acfe4af651c94964c1e3b263c3d75f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 12:01:36 +0200 Subject: [PATCH 009/122] fixmes --- coderd/notifications/enqueuer.go | 3 ++- coderd/notifications/notifier.go | 4 ++-- testutil/notifications.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/coderd/notifications/enqueuer.go b/coderd/notifications/enqueuer.go index 86642cb1b010e..9ee46c74db539 100644 --- a/coderd/notifications/enqueuer.go +++ b/coderd/notifications/enqueuer.go @@ -54,7 +54,8 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem // Enqueue queues a notification message for later delivery, assumes no structured input data. func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { - return s.EnqueueData(ctx, userID, templateID, labels, map[string]any{}, createdBy, targets...) + // "nil" data will be omitted while building the JSON payload. + return s.EnqueueData(ctx, userID, templateID, labels, nil, createdBy, targets...) } // Enqueue queues a notification message for later delivery. diff --git a/coderd/notifications/notifier.go b/coderd/notifications/notifier.go index 0bfaa04324327..ffa62ed603dff 100644 --- a/coderd/notifications/notifier.go +++ b/coderd/notifications/notifier.go @@ -221,10 +221,10 @@ func (n *notifier) prepare(ctx context.Context, msg database.AcquireNotification } var title, body string - if title, err = render.GoTemplate(msg.TitleTemplate, payload, nil); err != nil { + if title, err = render.GoTemplate(msg.TitleTemplate, payload, nil); err != nil { // FIXME helpers return nil, xerrors.Errorf("render title: %w", err) } - if body, err = render.GoTemplate(msg.BodyTemplate, payload, nil); err != nil { + if body, err = render.GoTemplate(msg.BodyTemplate, payload, nil); err != nil { // FIXME helpers return nil, xerrors.Errorf("render body: %w", err) } diff --git a/testutil/notifications.go b/testutil/notifications.go index df9983342a6a3..c9b4bd63c980a 100644 --- a/testutil/notifications.go +++ b/testutil/notifications.go @@ -21,7 +21,7 @@ type Notification struct { } func (f *FakeNotificationsEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { - return f.EnqueueData(ctx, userID, templateID, labels, map[string]any{}, createdBy, targets...) + return f.EnqueueData(ctx, userID, templateID, labels, nil, createdBy, targets...) } func (f *FakeNotificationsEnqueuer) EnqueueData(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { From 2fc383e4ad040d02b475a0db7f8c2b7c45a9d359 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 12:45:06 +0200 Subject: [PATCH 010/122] NoopEnqueuer --- coderd/notifications/enqueuer.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/coderd/notifications/enqueuer.go b/coderd/notifications/enqueuer.go index 9ee46c74db539..b67b2735bd8bf 100644 --- a/coderd/notifications/enqueuer.go +++ b/coderd/notifications/enqueuer.go @@ -169,3 +169,8 @@ func (*NoopEnqueuer) Enqueue(context.Context, uuid.UUID, uuid.UUID, map[string]s // nolint:nilnil // irrelevant. return nil, nil } + +func (*NoopEnqueuer) EnqueueData(context.Context, uuid.UUID, uuid.UUID, map[string]string, map[string]any, string, ...uuid.UUID) (*uuid.UUID, error) { + // nolint:nilnil // irrelevant. + return nil, nil +} From 96c4062e8db696468a5297d7ef1c43f61a812252 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 13:12:26 +0200 Subject: [PATCH 011/122] fixme helpers --- coderd/notifications/manager.go | 4 +++- coderd/notifications/notifier.go | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/coderd/notifications/manager.go b/coderd/notifications/manager.go index 6d8d200939880..3c983b2b3ee3d 100644 --- a/coderd/notifications/manager.go +++ b/coderd/notifications/manager.go @@ -46,6 +46,7 @@ type Manager struct { notifier *notifier handlers map[database.NotificationMethod]Handler method database.NotificationMethod + helpers template.FuncMap metrics *Metrics @@ -108,6 +109,7 @@ func NewManager(cfg codersdk.NotificationsConfig, store Store, helpers template. done: make(chan any), handlers: defaultHandlers(cfg, helpers, log), + helpers: helpers, clock: quartz.NewReal(), } @@ -169,7 +171,7 @@ func (m *Manager) loop(ctx context.Context) error { var eg errgroup.Group // Create a notifier to run concurrently, which will handle dequeueing and dispatching notifications. - m.notifier = newNotifier(m.cfg, uuid.New(), m.log, m.store, m.handlers, m.metrics, m.clock) + m.notifier = newNotifier(m.cfg, uuid.New(), m.log, m.store, m.handlers, m.helpers, m.metrics, m.clock) eg.Go(func() error { return m.notifier.run(ctx, m.success, m.failure) }) diff --git a/coderd/notifications/notifier.go b/coderd/notifications/notifier.go index ffa62ed603dff..a3ca9fc931aa1 100644 --- a/coderd/notifications/notifier.go +++ b/coderd/notifications/notifier.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "sync" + "text/template" "github.com/google/uuid" "golang.org/x/sync/errgroup" @@ -36,13 +37,14 @@ type notifier struct { handlers map[database.NotificationMethod]Handler metrics *Metrics + helpers template.FuncMap // clock is for testing clock quartz.Clock } func newNotifier(cfg codersdk.NotificationsConfig, id uuid.UUID, log slog.Logger, db Store, - hr map[database.NotificationMethod]Handler, metrics *Metrics, clock quartz.Clock, + hr map[database.NotificationMethod]Handler, helpers template.FuncMap, metrics *Metrics, clock quartz.Clock, ) *notifier { tick := clock.NewTicker(cfg.FetchInterval.Value(), "notifier", "fetchInterval") return ¬ifier{ @@ -54,6 +56,7 @@ func newNotifier(cfg codersdk.NotificationsConfig, id uuid.UUID, log slog.Logger tick: tick, store: db, handlers: hr, + helpers: helpers, metrics: metrics, clock: clock, } @@ -221,10 +224,10 @@ func (n *notifier) prepare(ctx context.Context, msg database.AcquireNotification } var title, body string - if title, err = render.GoTemplate(msg.TitleTemplate, payload, nil); err != nil { // FIXME helpers + if title, err = render.GoTemplate(msg.TitleTemplate, payload, n.helpers); err != nil { return nil, xerrors.Errorf("render title: %w", err) } - if body, err = render.GoTemplate(msg.BodyTemplate, payload, nil); err != nil { // FIXME helpers + if body, err = render.GoTemplate(msg.BodyTemplate, payload, n.helpers); err != nil { return nil, xerrors.Errorf("render body: %w", err) } From b89f484794b2d69e3a37a05719abf9dec9b2caf3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 14:36:55 +0200 Subject: [PATCH 012/122] Run generator --- cli/server.go | 5 +++++ coderd/notifications/reports/generator.go | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/server.go b/cli/server.go index 94f1518fa13a1..48ba471607fcc 100644 --- a/cli/server.go +++ b/cli/server.go @@ -56,6 +56,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/coderd/entitlements" + "github.com/coder/coder/v2/coderd/notifications/reports" "github.com/coder/pretty" "github.com/coder/quartz" "github.com/coder/retry" @@ -1023,6 +1024,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // nolint:gocritic // TODO: create own role. notificationsManager.Run(dbauthz.AsSystemRestricted(ctx)) + + // Run report generator to distribute periodic reports. + reportGenerator := reports.NewReportGenerator(ctx, logger, options.Database, options.NotificationsEnqueuer, quartz.NewReal()) + defer reportGenerator.Close() } // Wrap the server in middleware that redirects to the access URL if diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 45a4b7b0bcb29..adf1276b47301 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -10,6 +10,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/quartz" ) @@ -17,7 +18,7 @@ const ( delay = 5 * time.Minute ) -func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.Clock) io.Closer { +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, notificationsEnqueuer notifications.Enqueuer, clk quartz.Clock) io.Closer { closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) From c9f99c590343ca0534061a4292044ab0ea374684 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 14:43:41 +0200 Subject: [PATCH 013/122] fix --- coderd/notifications/reports/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index adf1276b47301..1afe9627d29df 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -18,7 +18,7 @@ const ( delay = 5 * time.Minute ) -func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, notificationsEnqueuer notifications.Enqueuer, clk quartz.Clock) io.Closer { +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) io.Closer { closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) From a4f486840d04cbc05aa064cd82f7f73a6a35ca69 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 17:18:19 +0200 Subject: [PATCH 014/122] TODO --- coderd/notifications/reports/generator.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 1afe9627d29df..ed776f05742c3 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -18,7 +18,7 @@ const ( delay = 5 * time.Minute ) -func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) io.Closer { +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, notificationsEnqueuer notifications.Enqueuer, clk quartz.Clock) io.Closer { closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) @@ -41,7 +41,16 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto return nil } - // TODO + // TODO: + // + // 1. for every user: + // 1. for every template they administrate: + // 1. for every enabled report: + // 1. check last run `report_generator_log` + // 2. generate report + // 3. send notification + // 4. upsert into `report_generator_log` + // 2. clean stale `report_generator_log` entries logger.Info(ctx, "report generator finished", slog.F("duration", clk.Since(start))) From 630c024af6ada3a8d5302274a48c23b80dfaae75 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 17:30:07 +0200 Subject: [PATCH 015/122] naming --- coderd/notifications/events.go | 2 +- coderd/notifications/notifications_test.go | 4 ++-- coderd/notifications/reports/generator.go | 13 ++++++------- ...plateWorkspaceBuildsFailedReport-body.md.golden} | 0 ...lateWorkspaceBuildsFailedReport-title.md.golden} | 0 5 files changed, 9 insertions(+), 10 deletions(-) rename coderd/notifications/testdata/rendered-templates/{TemplateWorkspaceBuildSummary-body.md.golden => TemplateWorkspaceBuildsFailedReport-body.md.golden} (100%) rename coderd/notifications/testdata/rendered-templates/{TemplateWorkspaceBuildSummary-title.md.golden => TemplateWorkspaceBuildsFailedReport-title.md.golden} (100%) diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 12e4a9026d68b..43406c3012317 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -30,5 +30,5 @@ var ( var ( TemplateTemplateDeleted = uuid.MustParse("29a09665-2a4c-403f-9648-54301670e7be") - TemplateWorkspaceBuildSummary = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") + TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") ) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 781baafee0a16..21eee9db66542 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -845,8 +845,8 @@ func TestNotificationTemplatesCanRender(t *testing.T) { }, }, { - name: "TemplateWorkspaceBuildSummary", - id: notifications.TemplateWorkspaceBuildSummary, + name: "TemplateWorkspaceBuildsFailedReport", + id: notifications.TemplateWorkspaceBuildsFailedReport, payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index ed776f05742c3..0af6c4ec62ee5 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -43,13 +43,12 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto // TODO: // - // 1. for every user: - // 1. for every template they administrate: - // 1. for every enabled report: - // 1. check last run `report_generator_log` - // 2. generate report - // 3. send notification - // 4. upsert into `report_generator_log` + // 1. select(workspace_builds_failed): templates + (template admins + users with "write" permissions) + matching entry for `report_generator_log`: + // 1. check last run `report_generator_log` + // 2. generate report + // 3. send notification + // 4. upsert into `report_generator_log` + // // 2. clean stale `report_generator_log` entries logger.Info(ctx, "report generator finished", slog.F("duration", clk.Since(start))) diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-body.md.golden similarity index 100% rename from coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-body.md.golden rename to coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-body.md.golden diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-title.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-title.md.golden similarity index 100% rename from coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildSummary-title.md.golden rename to coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-title.md.golden From 9a414f051f928b0d9f080446a9aff05edcfd0000 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Sep 2024 17:31:25 +0200 Subject: [PATCH 016/122] fix lint --- coderd/notifications/reports/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 0af6c4ec62ee5..6e34485157c9e 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -18,7 +18,7 @@ const ( delay = 5 * time.Minute ) -func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, notificationsEnqueuer notifications.Enqueuer, clk quartz.Clock) io.Closer { +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) io.Closer { closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) From aef1287702c599179e230f4e8e8871d9011f1a54 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 9 Sep 2024 13:03:32 +0200 Subject: [PATCH 017/122] TODO --- coderd/database/dbauthz/dbauthz.go | 8 +++++ coderd/database/dbmem/dbmem.go | 13 +++++++++ coderd/database/dbmetrics/dbmetrics.go | 14 +++++++++ coderd/database/dump.sql | 11 +++++++ .../migrations/000249_email_reports.down.sql | 2 ++ .../migrations/000249_email_reports.up.sql | 12 ++++++++ coderd/database/models.go | 9 +++++- coderd/database/querier.go | 6 +++- coderd/database/queries.sql.go | 29 ++++++++++++++++++- coderd/database/queries/notifications.sql | 9 ++++++ coderd/database/unique_constraint.go | 1 + coderd/notifications/reports/generator.go | 23 ++++++++++----- 12 files changed, 126 insertions(+), 11 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index f6bd03cc50e8b..74adc85591acb 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1144,6 +1144,10 @@ func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { return q.db.DeleteOldProvisionerDaemons(ctx) } +func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { + panic("not implemented") +} + func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err @@ -3906,6 +3910,10 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse return q.db.UpsertProvisionerDaemon(ctx, arg) } +func (q *querier) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { + panic("not implemented") +} + func (q *querier) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil { return database.TailnetAgent{}, err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b1d2178e66a29..bf3aa83bcd04b 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1706,6 +1706,10 @@ func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { return nil } +func (q *FakeQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { + panic("not implemented") +} + func (q *FakeQuerier) DeleteOldWorkspaceAgentLogs(_ context.Context, threshold time.Time) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -9186,6 +9190,15 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up return d, nil } +func (q *FakeQuerier) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + panic("not implemented") +} + func (*FakeQuerier) UpsertTailnetAgent(context.Context, database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { return database.TailnetAgent{}, ErrUnimplemented } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 38289c143bfd9..9f94e1e30f66e 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -305,6 +305,13 @@ func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error { return r0 } +func (m metricsStore) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { + start := time.Now() + r0 := m.s.DeleteOldReportGeneratorLogs(ctx, frequencyDays) + m.queryLatencies.WithLabelValues("DeleteOldReportGeneratorLogs").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) DeleteOldWorkspaceAgentLogs(ctx context.Context, arg time.Time) error { start := time.Now() r0 := m.s.DeleteOldWorkspaceAgentLogs(ctx, arg) @@ -2454,6 +2461,13 @@ func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database. return r0, r1 } +func (m metricsStore) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { + start := time.Now() + r0 := m.s.UpsertReportGeneratorLog(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertReportGeneratorLog").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { start := time.Now() r0, r1 := m.s.UpsertTailnetAgent(ctx, arg) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 989586dddf4ef..05a6e4d4a07f1 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -946,6 +946,14 @@ CREATE TABLE replicas ( "primary" boolean DEFAULT true NOT NULL ); +CREATE TABLE report_generator_logs ( + user_id uuid NOT NULL, + notification_template_id uuid NOT NULL, + last_generated_at timestamp with time zone +); + +COMMENT ON TABLE report_generator_logs IS 'Logs with generated reports for users.'; + CREATE TABLE site_configs ( key character varying(256) NOT NULL, value text NOT NULL @@ -1748,6 +1756,9 @@ ALTER TABLE ONLY provisioner_jobs ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); +ALTER TABLE ONLY report_generator_logs + ADD CONSTRAINT report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); + ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); diff --git a/coderd/database/migrations/000249_email_reports.down.sql b/coderd/database/migrations/000249_email_reports.down.sql index ade1beee5a558..7a39df5a02753 100644 --- a/coderd/database/migrations/000249_email_reports.down.sql +++ b/coderd/database/migrations/000249_email_reports.down.sql @@ -1 +1,3 @@ DELETE FROM notification_templates WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00'; + +DROP TABLE report_generator_logs; diff --git a/coderd/database/migrations/000249_email_reports.up.sql b/coderd/database/migrations/000249_email_reports.up.sql index 0e65fe7dd2337..fb9b36c850f24 100644 --- a/coderd/database/migrations/000249_email_reports.up.sql +++ b/coderd/database/migrations/000249_email_reports.up.sql @@ -18,3 +18,15 @@ We recommend reviewing these issues to ensure future builds are successful.', "url": "{{ base_url }}/workspaces?filter=template%3A{{.Labels.template_name}}" } ]'::jsonb); + +CREATE TABLE report_generator_logs +( + user_id uuid NOT NULL, + notification_template_id uuid NOT NULL, + last_generated_at timestamp with time zone, + + PRIMARY KEY (user_id, notification_template_id), + UNIQUE (user_id, notification_template_id) +); + +COMMENT ON TABLE report_generator_logs IS 'Logs with generated reports for users.'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 959609d82eb79..1f15324c7e8b6 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -2397,6 +2397,13 @@ type Replica struct { Primary bool `db:"primary" json:"primary"` } +// Logs with generated reports for users. +type ReportGeneratorLog struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` + LastGeneratedAt sql.NullTime `db:"last_generated_at" json:"last_generated_at"` +} + type SiteConfig struct { Key string `db:"key" json:"key"` Value string `db:"value" json:"value"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c614a03834a9b..9a5f201e864c9 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -87,6 +87,8 @@ type sqlcQuerier interface { // A provisioner daemon with "zeroed" last_seen_at column indicates possible // connectivity issues (no provisioner daemon activity since registration). DeleteOldProvisionerDaemons(ctx context.Context) error + // Delete report generator logs that have been created at least a +5m ago. + DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error // If an agent hasn't connected in the last 7 days, we purge it's logs. // Exception: if the logs are related to the latest build, we keep those around. // Logs can take up a lot of space, so it's important we clean up frequently. @@ -478,6 +480,8 @@ type sqlcQuerier interface { UpsertNotificationsSettings(ctx context.Context, value string) error UpsertOAuthSigningKey(ctx context.Context, value string) error UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) + // Insert or update report generator logs with recent activity. + UpsertReportGeneratorLog(ctx context.Context, arg UpsertReportGeneratorLogParams) error UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fc388e55247d0..27df1380ddf30 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -3460,6 +3460,16 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { return err } +const deleteOldReportGeneratorLogs = `-- name: DeleteOldReportGeneratorLogs :exec +DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT($1::int, ' days')::interval - INTERVAL '5 min') +` + +// Delete report generator logs that have been created at least a +5m ago. +func (q *sqlQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { + _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, frequencyDays) + return err +} + const enqueueNotificationMessage = `-- name: EnqueueNotificationMessage :exec INSERT INTO notification_messages (id, notification_template_id, user_id, method, payload, targets, created_by, created_at) VALUES ($1, @@ -3743,6 +3753,23 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg return result.RowsAffected() } +const upsertReportGeneratorLog = `-- name: UpsertReportGeneratorLog :exec +INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = $3 WHERE (user_id = $1 AND notification_template_id = $2) +` + +type UpsertReportGeneratorLogParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` + LastGeneratedAt sql.NullTime `db:"last_generated_at" json:"last_generated_at"` +} + +// Insert or update report generator logs with recent activity. +func (q *sqlQuerier) UpsertReportGeneratorLog(ctx context.Context, arg UpsertReportGeneratorLogParams) error { + _, err := q.db.ExecContext(ctx, upsertReportGeneratorLog, arg.UserID, arg.NotificationTemplateID, arg.LastGeneratedAt) + return err +} + const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec DELETE FROM oauth2_provider_apps WHERE id = $1 ` diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 983d0d56e40d4..3190949d20f33 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -173,3 +173,12 @@ SELECT * FROM notification_templates WHERE kind = @kind::notification_template_kind ORDER BY name ASC; + +-- name: UpsertReportGeneratorLog :exec +-- Insert or update report generator logs with recent activity. +INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = $3 WHERE (user_id = $1 AND notification_template_id = $2); + +-- name: DeleteOldReportGeneratorLogs :exec +-- Delete report generator logs that have been created at least a +5m ago. +DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT(@frequency_days::int, ' days')::interval - INTERVAL '5 min'); diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index b3bf72f8178b6..927bb15bfda32 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -46,6 +46,7 @@ const ( UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); + UniqueReportGeneratorLogsPkey UniqueConstraint = "report_generator_logs_pkey" // ALTER TABLE ONLY report_generator_logs ADD CONSTRAINT report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id); diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 6e34485157c9e..98af2e2f76114 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -41,15 +41,22 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto return nil } - // TODO: + // TODO Report - workspace_builds_failed: // - // 1. select(workspace_builds_failed): templates + (template admins + users with "write" permissions) + matching entry for `report_generator_log`: - // 1. check last run `report_generator_log` - // 2. generate report - // 3. send notification - // 4. upsert into `report_generator_log` - // - // 2. clean stale `report_generator_log` entries + // 1. Fetch template admins. + // 2. Fetch templates. + // 3. For every template: + // 1. Fetch failed builds. + // 2. If failed builds == 0, continue. + // 3. Render the report. + // 4. Fetch template RW users. + // 5. For user := range template admins + RW users: + // 1. Check if report is enabled for the person. + // 2. Check `report_generator_log`. + // 3. If sent recently, continue + // 4. Send notification + // 5. Upsert into `report_generator_log`. + // 4. clean stale `report_generator_log` entries logger.Info(ctx, "report generator finished", slog.F("duration", clk.Since(start))) From f0f6df2f690e78160d7db5ddd549bf5bb0a32d7d Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 9 Sep 2024 11:45:01 +0000 Subject: [PATCH 018/122] fix dbmock --- coderd/database/dbmock/dbmock.go | 28 ++++++++++++++++++++++++++++ coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 1771807f26b2f..a2b31fad51c3d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -500,6 +500,20 @@ func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldProvisionerDaemons", reflect.TypeOf((*MockStore)(nil).DeleteOldProvisionerDaemons), arg0) } +// DeleteOldReportGeneratorLogs mocks base method. +func (m *MockStore) DeleteOldReportGeneratorLogs(arg0 context.Context, arg1 int32) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOldReportGeneratorLogs", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteOldReportGeneratorLogs indicates an expected call of DeleteOldReportGeneratorLogs. +func (mr *MockStoreMockRecorder) DeleteOldReportGeneratorLogs(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldReportGeneratorLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldReportGeneratorLogs), arg0, arg1) +} + // DeleteOldWorkspaceAgentLogs mocks base method. func (m *MockStore) DeleteOldWorkspaceAgentLogs(arg0 context.Context, arg1 time.Time) error { m.ctrl.T.Helper() @@ -5151,6 +5165,20 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) } +// UpsertReportGeneratorLog mocks base method. +func (m *MockStore) UpsertReportGeneratorLog(arg0 context.Context, arg1 database.UpsertReportGeneratorLogParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertReportGeneratorLog", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertReportGeneratorLog indicates an expected call of UpsertReportGeneratorLog. +func (mr *MockStoreMockRecorder) UpsertReportGeneratorLog(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertReportGeneratorLog), arg0, arg1) +} + // UpsertTailnetAgent mocks base method. func (m *MockStore) UpsertTailnetAgent(arg0 context.Context, arg1 database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { m.ctrl.T.Helper() diff --git a/coderd/database/models.go b/coderd/database/models.go index 1f15324c7e8b6..723cef1c69fe2 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 9a5f201e864c9..c5086534e72c9 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 27df1380ddf30..37544beb4a9bd 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database From f4e34a730c80d955f757702be0003964238c46a3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 9 Sep 2024 15:40:25 +0200 Subject: [PATCH 019/122] WIP --- coderd/notifications/reports/generator.go | 64 +++++++++++++++++------ 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 98af2e2f76114..8b248819031fa 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -2,15 +2,18 @@ package reports import ( "context" + "database/sql" "io" "time" "cdr.dev/slog" + "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/codersdk" "github.com/coder/quartz" ) @@ -18,7 +21,7 @@ const ( delay = 5 * time.Minute ) -func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) io.Closer { +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, enqueur notifications.Enqueuer, clk quartz.Clock) io.Closer { closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) @@ -41,22 +44,11 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto return nil } - // TODO Report - workspace_builds_failed: - // - // 1. Fetch template admins. - // 2. Fetch templates. - // 3. For every template: - // 1. Fetch failed builds. - // 2. If failed builds == 0, continue. - // 3. Render the report. - // 4. Fetch template RW users. - // 5. For user := range template admins + RW users: - // 1. Check if report is enabled for the person. - // 2. Check `report_generator_log`. - // 3. If sent recently, continue - // 4. Send notification - // 5. Upsert into `report_generator_log`. - // 4. clean stale `report_generator_log` entries + err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueur, clk) + if err != nil { + logger.Debug(ctx, "unable to report failed workspace builds") + return err + } logger.Info(ctx, "report generator finished", slog.F("duration", clk.Since(start))) @@ -98,3 +90,41 @@ func (i *reportGenerator) Close() error { <-i.closed return nil } + +func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) error { + const frequencyDays = 7 + + templateAdmins, err := db.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin}, + }) + if err != nil { + return xerrors.Errorf("unable to fetch template admins: %w", err) + } + + templates, err := db.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ + Deleted: false, + Deprecated: sql.NullBool{Bool: false, Valid: true}, + }) + if err != nil { + return xerrors.Errorf("unable to fetch active templates: %w", err) + } + + for _, template := range templates { + // 1. Fetch failed builds. + // 2. If failed builds == 0, continue. + // 3. Render the report. + // 4. Fetch template RW users. + // 5. For user := range template admins + RW users: + // 1. Check if report is enabled for the person. + // 2. Check `report_generator_log`. + // 3. If sent recently, continue + // 4. Send notification + // 5. Upsert into `report_generator_log`. + } + + err = db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) + if err != nil { + return xerrors.Errorf("unable to delete old report generator logs: %w", err) + } + return nil +} From 968bd241bd68dea383f33532e908776d51725d79 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 9 Sep 2024 17:05:59 +0200 Subject: [PATCH 020/122] TODOs --- coderd/notifications/reports/generator.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 8b248819031fa..ecfb27c225cd3 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -112,14 +112,14 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat for _, template := range templates { // 1. Fetch failed builds. // 2. If failed builds == 0, continue. - // 3. Render the report. - // 4. Fetch template RW users. - // 5. For user := range template admins + RW users: + // 3. Fetch template RW users. + // 4. For user := range template admins + RW users: // 1. Check if report is enabled for the person. // 2. Check `report_generator_log`. // 3. If sent recently, continue - // 4. Send notification - // 5. Upsert into `report_generator_log`. + // 4. Lazy-render the report. + // 5. Send notification + // 6. Upsert into `report_generator_log`. } err = db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) From e771c052779af9a6c8aa964dcff43e16e1b99ef0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 10:25:38 +0200 Subject: [PATCH 021/122] WIP --- coderd/database/dbauthz/dbauthz.go | 4 ++ coderd/database/dbmem/dbmem.go | 9 +++ coderd/database/dbmetrics/dbmetrics.go | 7 +++ coderd/database/dbmock/dbmock.go | 15 +++++ coderd/database/models.go | 2 +- coderd/database/querier.go | 3 +- coderd/database/queries.sql.go | 67 ++++++++++++++++++++- coderd/database/queries/workspacebuilds.sql | 19 ++++++ 8 files changed, 123 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 74adc85591acb..af2f072377894 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1434,6 +1434,10 @@ func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid. return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLinksByUserID)(ctx, userID) } +func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { + panic("not implemented") +} + func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { file, err := q.db.GetFileByHashAndCreator(ctx, arg) if err != nil { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index bf3aa83bcd04b..896997e0b9bdc 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -2511,6 +2511,15 @@ func (q *FakeQuerier) GetExternalAuthLinksByUserID(_ context.Context, userID uui return gals, nil } +func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + panic("not implemented") +} + func (q *FakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { if err := validateDatabaseType(arg); err != nil { return database.File{}, err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 9f94e1e30f66e..8c8dd2ee2de40 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -613,6 +613,13 @@ func (m metricsStore) GetExternalAuthLinksByUserID(ctx context.Context, userID u return r0, r1 } +func (m metricsStore) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { + start := time.Now() + r0, r1 := m.s.GetFailedWorkspaceBuildsByTemplateID(ctx, arg) + m.queryLatencies.WithLabelValues("GetFailedWorkspaceBuildsByTemplateID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { start := time.Now() file, err := m.s.GetFileByHashAndCreator(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index a2b31fad51c3d..649f1953274f1 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1208,6 +1208,21 @@ func (mr *MockStoreMockRecorder) GetExternalAuthLinksByUserID(arg0, arg1 any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExternalAuthLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetExternalAuthLinksByUserID), arg0, arg1) } +// GetFailedWorkspaceBuildsByTemplateID mocks base method. +func (m *MockStore) GetFailedWorkspaceBuildsByTemplateID(arg0 context.Context, arg1 database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFailedWorkspaceBuildsByTemplateID", arg0, arg1) + ret0, _ := ret[0].([]database.WorkspaceBuild) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFailedWorkspaceBuildsByTemplateID indicates an expected call of GetFailedWorkspaceBuildsByTemplateID. +func (mr *MockStoreMockRecorder) GetFailedWorkspaceBuildsByTemplateID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFailedWorkspaceBuildsByTemplateID", reflect.TypeOf((*MockStore)(nil).GetFailedWorkspaceBuildsByTemplateID), arg0, arg1) +} + // GetFileByHashAndCreator mocks base method. func (m *MockStore) GetFileByHashAndCreator(arg0 context.Context, arg1 database.GetFileByHashAndCreatorParams) (database.File, error) { m.ctrl.T.Helper() diff --git a/coderd/database/models.go b/coderd/database/models.go index 723cef1c69fe2..ea7873f01e8ce 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c5086534e72c9..7ee79bd4dac70 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -142,6 +142,7 @@ type sqlcQuerier interface { GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) + GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]WorkspaceBuild, error) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) // Get all templates that use a file. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 37544beb4a9bd..f013c378d8bd6 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -12501,6 +12501,71 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t return items, nil } +const getFailedWorkspaceBuildsByTemplateID = `-- name: GetFailedWorkspaceBuildsByTemplateID :many +SELECT + wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username +FROM + workspace_build_with_user AS wb +JOIN + workspaces AS w +ON + wb.workspace_id = w.id +JOIN + provisioner_jobs AS pj +ON + wb.job_id = pj.id +WHERE + w.template_id = $1 + AND wb.created_at > $2 + AND pj.completed_at IS NOT NULL + AND pj.job_status = 'failed' +` + +type GetFailedWorkspaceBuildsByTemplateIDParams struct { + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]WorkspaceBuild, error) { + rows, err := q.db.QueryContext(ctx, getFailedWorkspaceBuildsByTemplateID, arg.TemplateID, arg.CreatedAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceBuild + for rows.Next() { + var i WorkspaceBuild + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.WorkspaceID, + &i.TemplateVersionID, + &i.BuildNumber, + &i.Transition, + &i.InitiatorID, + &i.ProvisionerState, + &i.JobID, + &i.Deadline, + &i.Reason, + &i.DailyCost, + &i.MaxDeadline, + &i.InitiatorByAvatarUrl, + &i.InitiatorByUsername, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 2a1107ef75c5c..7e59815512e4f 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -179,3 +179,22 @@ WHERE wb.transition = 'start'::workspace_transition AND pj.completed_at IS NOT NULL; + +-- name: GetFailedWorkspaceBuildsByTemplateID :many +SELECT + wb.* +FROM + workspace_build_with_user AS wb +JOIN + workspaces AS w +ON + wb.workspace_id = w.id +JOIN + provisioner_jobs AS pj +ON + wb.job_id = pj.id +WHERE + w.template_id = $1 + AND wb.created_at > $2 + AND pj.completed_at IS NOT NULL + AND pj.job_status = 'failed'; From 33b1e994169ef758bb9c992c139cdf27a6dec383 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 08:47:00 +0000 Subject: [PATCH 022/122] fix --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index ea7873f01e8ce..723cef1c69fe2 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7ee79bd4dac70..b5c6d7c98c521 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f013c378d8bd6..c149a8f68fee5 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database From f61408064d2525c3944d8411c0d62330edb8d1c9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 08:49:55 +0000 Subject: [PATCH 023/122] WIP --- coderd/database/queries.sql.go | 4 ++-- coderd/database/queries/workspacebuilds.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c149a8f68fee5..17b6bfdcb4717 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12523,11 +12523,11 @@ WHERE type GetFailedWorkspaceBuildsByTemplateIDParams struct { TemplateID uuid.UUID `db:"template_id" json:"template_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` + Since time.Time `db:"since" json:"since"` } func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]WorkspaceBuild, error) { - rows, err := q.db.QueryContext(ctx, getFailedWorkspaceBuildsByTemplateID, arg.TemplateID, arg.CreatedAt) + rows, err := q.db.QueryContext(ctx, getFailedWorkspaceBuildsByTemplateID, arg.TemplateID, arg.Since) if err != nil { return nil, err } diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 7e59815512e4f..e346f27c1ec09 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -195,6 +195,6 @@ ON wb.job_id = pj.id WHERE w.template_id = $1 - AND wb.created_at > $2 + AND wb.created_at > @since AND pj.completed_at IS NOT NULL AND pj.job_status = 'failed'; From 4d7a304e533d4ca56bfb6d72c4c0acb710bf4a5f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 12:03:16 +0200 Subject: [PATCH 024/122] WIP --- coderd/database/dump.sql | 2 +- .../migrations/000249_email_reports.up.sql | 2 +- coderd/database/models.go | 8 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 8 +- coderd/notifications/reports/generator.go | 124 +++++++++++++++--- 6 files changed, 116 insertions(+), 30 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 05a6e4d4a07f1..d3f58aa40c712 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -949,7 +949,7 @@ CREATE TABLE replicas ( CREATE TABLE report_generator_logs ( user_id uuid NOT NULL, notification_template_id uuid NOT NULL, - last_generated_at timestamp with time zone + last_generated_at timestamp with time zone NOT NULL ); COMMENT ON TABLE report_generator_logs IS 'Logs with generated reports for users.'; diff --git a/coderd/database/migrations/000249_email_reports.up.sql b/coderd/database/migrations/000249_email_reports.up.sql index fb9b36c850f24..3285f0680821b 100644 --- a/coderd/database/migrations/000249_email_reports.up.sql +++ b/coderd/database/migrations/000249_email_reports.up.sql @@ -23,7 +23,7 @@ CREATE TABLE report_generator_logs ( user_id uuid NOT NULL, notification_template_id uuid NOT NULL, - last_generated_at timestamp with time zone, + last_generated_at timestamp with time zone NOT NULL, PRIMARY KEY (user_id, notification_template_id), UNIQUE (user_id, notification_template_id) diff --git a/coderd/database/models.go b/coderd/database/models.go index 723cef1c69fe2..b2c5f851b7f78 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -2399,9 +2399,9 @@ type Replica struct { // Logs with generated reports for users. type ReportGeneratorLog struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` - LastGeneratedAt sql.NullTime `db:"last_generated_at" json:"last_generated_at"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` + LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` } type SiteConfig struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b5c6d7c98c521..7ee79bd4dac70 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 17b6bfdcb4717..8a2b69fe74663 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -3759,9 +3759,9 @@ ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at ` type UpsertReportGeneratorLogParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` - LastGeneratedAt sql.NullTime `db:"last_generated_at" json:"last_generated_at"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` + LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` } // Insert or update report generator logs with recent activity. diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index ecfb27c225cd3..5a2e39d9e5c60 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -4,11 +4,16 @@ import ( "context" "database/sql" "io" + "slices" + "sort" "time" - "cdr.dev/slog" "golang.org/x/xerrors" + "cdr.dev/slog" + + "github.com/google/uuid" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -91,16 +96,9 @@ func (i *reportGenerator) Close() error { return nil } -func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, _ notifications.Enqueuer, clk quartz.Clock) error { +func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { const frequencyDays = 7 - templateAdmins, err := db.GetUsers(ctx, database.GetUsersParams{ - RbacRole: []string{codersdk.RoleTemplateAdmin}, - }) - if err != nil { - return xerrors.Errorf("unable to fetch template admins: %w", err) - } - templates, err := db.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ Deleted: false, Deprecated: sql.NullBool{Bool: false, Valid: true}, @@ -110,16 +108,69 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, template := range templates { - // 1. Fetch failed builds. - // 2. If failed builds == 0, continue. - // 3. Fetch template RW users. - // 4. For user := range template admins + RW users: - // 1. Check if report is enabled for the person. - // 2. Check `report_generator_log`. - // 3. If sent recently, continue - // 4. Lazy-render the report. - // 5. Send notification - // 6. Upsert into `report_generator_log`. + failedBuilds, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ + TemplateID: template.ID, + Since: dbtime.Time(clk.Now()).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", template.ID), slog.Error(err)) + continue + } + + templateAdmins, err := findTemplateAdmins(ctx, db, template) + if err != nil { + logger.Error(ctx, "unable to find template admins", slog.F("template_id", template.ID), slog.Error(err)) + continue + } + + for _, templateAdmin := range templateAdmins { + // TODO Check if report is enabled for the person. + // TODO Check `report_generator_log`. + // TODO If sent recently, continue + + if len(failedBuilds) == 0 { + err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ + UserID: templateAdmin.ID, + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) + continue + } + } + + // TODO Lazy-render the report. + reportData := map[string]any{} + + templateDisplayName := template.DisplayName + if templateDisplayName == "" { + templateDisplayName = template.Name + } + + if _, err := enqueuer.EnqueueData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, + map[string]string{ + "template_name": template.Name, + "template_display_name": templateDisplayName, + }, + reportData, + "report_generator", + template.ID, template.OrganizationID, + ); err != nil { + logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err)) + } + + err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ + UserID: templateAdmin.ID, + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) + continue + } + } + } err = db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) @@ -128,3 +179,38 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } return nil } + +func findTemplateAdmins(ctx context.Context, db database.Store, template database.Template) ([]database.GetUsersRow, error) { + users, err := db.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleTemplateAdmin}, + }) + if err != nil { + return nil, xerrors.Errorf("unable to fetch template admins: %w", err) + } + + usersByIDs := map[uuid.UUID]database.GetUsersRow{} + var userIDs []uuid.UUID + for _, user := range users { + usersByIDs[user.ID] = user + userIDs = append(userIDs, user.ID) + } + + var templateAdmins []database.GetUsersRow + if len(userIDs) > 0 { + orgIDsByMemberIDs, err := db.GetOrganizationIDsByMemberIDs(ctx, userIDs) + if err != nil { + return nil, xerrors.Errorf("unable to fetch organization IDs by member IDs: %w", err) + } + + for _, entry := range orgIDsByMemberIDs { + if slices.Contains(entry.OrganizationIDs, template.OrganizationID) { + templateAdmins = append(templateAdmins, usersByIDs[entry.UserID]) + } + } + } + sort.Slice(templateAdmins, func(i, j int) bool { + return templateAdmins[i].Username < templateAdmins[j].Username + }) + + return templateAdmins, nil +} From d5c212e31559f8b68b476b8db6fe1349c1055635 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 13:13:55 +0200 Subject: [PATCH 025/122] WIP --- coderd/database/dbauthz/dbauthz.go | 8 ++++++++ coderd/database/dbmem/dbmem.go | 18 +++++++++++++++++ coderd/database/dbmetrics/dbmetrics.go | 14 +++++++++++++ coderd/database/dbmock/dbmock.go | 15 ++++++++++++++ coderd/database/querier.go | 2 ++ coderd/database/queries.sql.go | 23 ++++++++++++++++++++++ coderd/database/queries/notifications.sql | 10 ++++++++++ coderd/notifications/reports/generator.go | 24 +++++++++++++++++++++-- 8 files changed, 112 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index af2f072377894..773e888dae006 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -898,6 +898,10 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } +func (q *querier) GetReportGeneratorLogByUserAndKind(ctx context.Context, arg database.GetReportGeneratorLogByUserAndKindParams) (database.ReportGeneratorLog, error) { + panic("not implemented") +} + func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } @@ -1864,6 +1868,10 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti return q.db.GetReplicasUpdatedAfter(ctx, updatedAt) } +func (q *querier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + panic("not implemented") +} + func (q *querier) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 896997e0b9bdc..7c965faeaaa7d 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -991,6 +991,15 @@ func (q *FakeQuerier) getOrganizationByIDNoLock(id uuid.UUID) (database.Organiza return database.Organization{}, sql.ErrNoRows } +func (q *FakeQuerier) GetReportGeneratorLogByUserAndKind(ctx context.Context, arg database.GetReportGeneratorLogByUserAndKindParams) (database.ReportGeneratorLog, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.ReportGeneratorLog{}, err + } + + panic("not implemented") +} + func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -3518,6 +3527,15 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time. return replicas, nil } +func (q *FakeQuerier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.ReportGeneratorLog{}, err + } + + panic("not implemented") +} + func (*FakeQuerier) GetTailnetAgents(context.Context, uuid.UUID) ([]database.TailnetAgent, error) { return nil, ErrUnimplemented } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 8c8dd2ee2de40..cf2661e9bacf3 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -81,6 +81,13 @@ func (m metricsStore) InTx(f func(database.Store) error, options *sql.TxOptions) return err } +func (m metricsStore) GetReportGeneratorLogByUserAndKind(ctx context.Context, arg database.GetReportGeneratorLogByUserAndKindParams) (database.ReportGeneratorLog, error) { + start := time.Now() + r0, r1 := m.s.GetReportGeneratorLogByUserAndKind(ctx, arg) + m.queryLatencies.WithLabelValues("GetReportGeneratorLogByUserAndKind").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error { start := time.Now() err := m.s.AcquireLock(ctx, pgAdvisoryXactLock) @@ -1005,6 +1012,13 @@ func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt tim return replicas, err } +func (m metricsStore) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + start := time.Now() + r0, r1 := m.s.GetReportGeneratorLogByUserAndTemplate(ctx, arg) + m.queryLatencies.WithLabelValues("GetReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) { start := time.Now() r0, r1 := m.s.GetTailnetAgents(ctx, id) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 649f1953274f1..ecc4c1950edcd 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2048,6 +2048,21 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1) } +// GetReportGeneratorLogByUserAndTemplate mocks base method. +func (m *MockStore) GetReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReportGeneratorLogByUserAndTemplate", arg0, arg1) + ret0, _ := ret[0].(database.ReportGeneratorLog) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetReportGeneratorLogByUserAndTemplate indicates an expected call of GetReportGeneratorLogByUserAndTemplate. +func (mr *MockStoreMockRecorder) GetReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetReportGeneratorLogByUserAndTemplate), arg0, arg1) +} + // GetTailnetAgents mocks base method. func (m *MockStore) GetTailnetAgents(arg0 context.Context, arg1 uuid.UUID) ([]database.TailnetAgent, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7ee79bd4dac70..ad8f411773b6b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -202,6 +202,8 @@ type sqlcQuerier interface { GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error) GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) + // Fetch the report generator log indicating recent activity. + GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8a2b69fe74663..18ab4c15b0ce6 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3665,6 +3665,29 @@ func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind No return items, nil } +const getReportGeneratorLogByUserAndTemplate = `-- name: GetReportGeneratorLogByUserAndTemplate :one +SELECT + user_id, notification_template_id, last_generated_at +FROM + report_generator_logs +WHERE + user_id = $1 + AND notification_template_id = $2 +` + +type GetReportGeneratorLogByUserAndTemplateParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` +} + +// Fetch the report generator log indicating recent activity. +func (q *sqlQuerier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) { + row := q.db.QueryRowContext(ctx, getReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) + var i ReportGeneratorLog + err := row.Scan(&i.UserID, &i.NotificationTemplateID, &i.LastGeneratedAt) + return i, err +} + const getUserNotificationPreferences = `-- name: GetUserNotificationPreferences :many SELECT user_id, notification_template_id, disabled, created_at, updated_at FROM notification_preferences diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 3190949d20f33..b4b45884cd939 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -174,6 +174,16 @@ FROM notification_templates WHERE kind = @kind::notification_template_kind ORDER BY name ASC; +-- name: GetReportGeneratorLogByUserAndTemplate :one +-- Fetch the report generator log indicating recent activity. +SELECT + * +FROM + report_generator_logs +WHERE + user_id = $1 + AND notification_template_id = $2; + -- name: UpsertReportGeneratorLog :exec -- Insert or update report generator logs with recent activity. INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 5a2e39d9e5c60..dac5bc54a550e 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -125,10 +125,30 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat for _, templateAdmin := range templateAdmins { // TODO Check if report is enabled for the person. - // TODO Check `report_generator_log`. - // TODO If sent recently, continue + + reportLog, err := db.GetReportGeneratorLogByUserAndTemplate(ctx, database.GetReportGeneratorLogByUserAndTemplateParams{ + UserID: templateAdmin.ID, + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + }) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { // sql.ErrNoRows: report not generated yet + return xerrors.Errorf("unable to get recent report generator log for user: %w", err) + } + + if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(frequencyDays*24*time.Hour).After(clk.Now()) { + // report generated recently, no need to send it now + err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ + UserID: templateAdmin.ID, + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) + continue + } + } if len(failedBuilds) == 0 { + // no failed workspace builds, no need to send the report err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, From 0ab35f13a241f88b217c2d3d84c4dc3caa88bc04 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 11:14:59 +0000 Subject: [PATCH 026/122] WIP --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index b2c5f851b7f78..acd5c23c8879a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index ad8f411773b6b..8fd10096d396e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 18ab4c15b0ce6..72010eace7a78 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database From fc804bd4830d69dcd5930d6d8bf05983068ec4db Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 14:46:10 +0200 Subject: [PATCH 027/122] another WIP --- coderd/notifications/reports/generator.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index dac5bc54a550e..ffaf363e7a1f9 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -117,6 +117,9 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat continue } + // TODO Lazy-render the report. + reportData := map[string]any{} + templateAdmins, err := findTemplateAdmins(ctx, db, template) if err != nil { logger.Error(ctx, "unable to find template admins", slog.F("template_id", template.ID), slog.Error(err)) @@ -160,9 +163,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } - // TODO Lazy-render the report. - reportData := map[string]any{} - templateDisplayName := template.DisplayName if templateDisplayName == "" { templateDisplayName = template.Name @@ -190,7 +190,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat continue } } - } err = db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) @@ -200,6 +199,13 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return nil } +func buildDataForReportFailedWorkspaceBuilds() map[string]any { + // TODO Lazy-render the report. + reportData := map[string]any{} + + return reportData +} + func findTemplateAdmins(ctx context.Context, db database.Store, template database.Template) ([]database.GetUsersRow, error) { users, err := db.GetUsers(ctx, database.GetUsersParams{ RbacRole: []string{codersdk.RoleTemplateAdmin}, From c25155d36d646f2a860b29e59ad95b6a5ae04627 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 15:24:09 +0200 Subject: [PATCH 028/122] WIP --- coderd/database/dbauthz/dbauthz.go | 6 +- coderd/database/dbmem/dbmem.go | 8 ++- coderd/database/dbmetrics/dbmetrics.go | 13 ++++- coderd/database/dbmock/dbmock.go | 15 +++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 65 +++++++++++++++++++++ coderd/database/queries/notifications.sql | 4 +- coderd/database/queries/workspacebuilds.sql | 25 ++++++++ coderd/notifications/reports/generator.go | 60 ++++++++++--------- 9 files changed, 160 insertions(+), 37 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 773e888dae006..6d9072dbf2b39 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -898,7 +898,7 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } -func (q *querier) GetReportGeneratorLogByUserAndKind(ctx context.Context, arg database.GetReportGeneratorLogByUserAndKindParams) (database.ReportGeneratorLog, error) { +func (q *querier) GetWorkspaceBuildStats(ctx context.Context, arg database.GetWorkspaceBuildStatsParams) ([]database.GetWorkspaceBuildStatsRow, error) { panic("not implemented") } @@ -2475,6 +2475,10 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID) } +func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { + panic("not implemented") +} + func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil { return nil, err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7c965faeaaa7d..3192db0b1e68c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -991,10 +991,10 @@ func (q *FakeQuerier) getOrganizationByIDNoLock(id uuid.UUID) (database.Organiza return database.Organization{}, sql.ErrNoRows } -func (q *FakeQuerier) GetReportGeneratorLogByUserAndKind(ctx context.Context, arg database.GetReportGeneratorLogByUserAndKindParams) (database.ReportGeneratorLog, error) { +func (q *FakeQuerier) GetWorkspaceBuildStats(ctx context.Context, arg database.GetWorkspaceBuildStatsParams) ([]database.GetWorkspaceBuildStatsRow, error) { err := validateDatabaseType(arg) if err != nil { - return database.ReportGeneratorLog{}, err + return nil, err } panic("not implemented") @@ -5823,6 +5823,10 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu return params, nil } +func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { + panic("not implemented") +} + func (q *FakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, params database.GetWorkspaceBuildsByWorkspaceIDParams, ) ([]database.WorkspaceBuild, error) { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index cf2661e9bacf3..98524d7326deb 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -81,10 +81,10 @@ func (m metricsStore) InTx(f func(database.Store) error, options *sql.TxOptions) return err } -func (m metricsStore) GetReportGeneratorLogByUserAndKind(ctx context.Context, arg database.GetReportGeneratorLogByUserAndKindParams) (database.ReportGeneratorLog, error) { +func (m metricsStore) GetWorkspaceBuildStats(ctx context.Context, arg database.GetWorkspaceBuildStatsParams) ([]database.GetWorkspaceBuildStatsRow, error) { start := time.Now() - r0, r1 := m.s.GetReportGeneratorLogByUserAndKind(ctx, arg) - m.queryLatencies.WithLabelValues("GetReportGeneratorLogByUserAndKind").Observe(time.Since(start).Seconds()) + r0, r1 := m.s.GetWorkspaceBuildStats(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaceBuildStats").Observe(time.Since(start).Seconds()) return r0, r1 } @@ -1453,6 +1453,13 @@ func (m metricsStore) GetWorkspaceBuildParameters(ctx context.Context, workspace return params, err } +func (m metricsStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceBuildStatsByTemplates(ctx, since) + m.queryLatencies.WithLabelValues("GetWorkspaceBuildStatsByTemplates").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { start := time.Now() builds, err := m.s.GetWorkspaceBuildsByWorkspaceID(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index ecc4c1950edcd..bd16560cf4484 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3023,6 +3023,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceBuildParameters(arg0, arg1 any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildParameters), arg0, arg1) } +// GetWorkspaceBuildStatsByTemplates mocks base method. +func (m *MockStore) GetWorkspaceBuildStatsByTemplates(arg0 context.Context, arg1 time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceBuildStatsByTemplates", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspaceBuildStatsByTemplatesRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceBuildStatsByTemplates indicates an expected call of GetWorkspaceBuildStatsByTemplates. +func (mr *MockStoreMockRecorder) GetWorkspaceBuildStatsByTemplates(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildStatsByTemplates", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildStatsByTemplates), arg0, arg1) +} + // GetWorkspaceBuildsByWorkspaceID mocks base method. func (m *MockStore) GetWorkspaceBuildsByWorkspaceID(arg0 context.Context, arg1 database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 8fd10096d396e..3559baab9a40d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -305,6 +305,7 @@ type sqlcQuerier interface { GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) + GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (GetWorkspaceByAgentIDRow, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 72010eace7a78..cb2621892f328 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12847,6 +12847,71 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co return i, err } +const getWorkspaceBuildStatsByTemplates = `-- name: GetWorkspaceBuildStatsByTemplates :many +SELECT + w.template_id, + t.name AS template_name, + t.display_name AS template_display_name, + t.organization_id AS template_organization_id, + COUNT(*) AS total_builds, + COUNT(CASE WHEN pj.job_status = 'failed' THEN 1 END) AS failed_builds +FROM + workspace_build_with_user AS wb +JOIN + workspaces AS w ON + wb.workspace_id = w.id +JOIN + provisioner_jobs AS pj ON + wb.job_id = pj.id +JOIN + templates AS t ON + w.template_id = t.id +WHERE + wb.created_at > $1 + AND pj.completed_at IS NOT NULL +GROUP BY + w.template_id, template_name, template_display_name, template_organization_id +` + +type GetWorkspaceBuildStatsByTemplatesRow struct { + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + TemplateName string `db:"template_name" json:"template_name"` + TemplateDisplayName string `db:"template_display_name" json:"template_display_name"` + TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"` + TotalBuilds int64 `db:"total_builds" json:"total_builds"` + FailedBuilds int64 `db:"failed_builds" json:"failed_builds"` +} + +func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceBuildStatsByTemplates, since) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWorkspaceBuildStatsByTemplatesRow + for rows.Next() { + var i GetWorkspaceBuildStatsByTemplatesRow + if err := rows.Scan( + &i.TemplateID, + &i.TemplateName, + &i.TemplateDisplayName, + &i.TemplateOrganizationID, + &i.TotalBuilds, + &i.FailedBuilds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index b4b45884cd939..61011502f1101 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -190,5 +190,5 @@ INSERT INTO report_generator_logs (user_id, notification_template_id, last_gener ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = $3 WHERE (user_id = $1 AND notification_template_id = $2); -- name: DeleteOldReportGeneratorLogs :exec --- Delete report generator logs that have been created at least a +5m ago. -DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT(@frequency_days::int, ' days')::interval - INTERVAL '5 min'); +-- Delete report generator logs that have been created at least a +1h ago. +DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT(@frequency_days::int, ' days')::interval - INTERVAL '1 hour'); diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index e346f27c1ec09..17ef71ba09ce1 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -180,6 +180,31 @@ WHERE AND pj.completed_at IS NOT NULL; +-- name: GetWorkspaceBuildStatsByTemplates :many +SELECT + w.template_id, + t.name AS template_name, + t.display_name AS template_display_name, + t.organization_id AS template_organization_id, + COUNT(*) AS total_builds, + COUNT(CASE WHEN pj.job_status = 'failed' THEN 1 END) AS failed_builds +FROM + workspace_build_with_user AS wb +JOIN + workspaces AS w ON + wb.workspace_id = w.id +JOIN + provisioner_jobs AS pj ON + wb.job_id = pj.id +JOIN + templates AS t ON + w.template_id = t.id +WHERE + wb.created_at > @since + AND pj.completed_at IS NOT NULL +GROUP BY + w.template_id, template_name, template_display_name, template_organization_id; + -- name: GetFailedWorkspaceBuildsByTemplateID :many SELECT wb.* diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index ffaf363e7a1f9..3bad00bd89621 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -12,6 +12,7 @@ import ( "cdr.dev/slog" + "github.com/coder/quartz" "github.com/google/uuid" "github.com/coder/coder/v2/coderd/database" @@ -19,11 +20,10 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/codersdk" - "github.com/coder/quartz" ) const ( - delay = 5 * time.Minute + delay = 15 * time.Minute ) func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, enqueur notifications.Enqueuer, clk quartz.Clock) io.Closer { @@ -99,30 +99,32 @@ func (i *reportGenerator) Close() error { func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { const frequencyDays = 7 - templates, err := db.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ - Deleted: false, - Deprecated: sql.NullBool{Bool: false, Valid: true}, - }) + statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(clk.Now()).UTC()) if err != nil { - return xerrors.Errorf("unable to fetch active templates: %w", err) + return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) } - for _, template := range templates { - failedBuilds, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ - TemplateID: template.ID, - Since: dbtime.Time(clk.Now()).UTC(), - }) - if err != nil { - logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", template.ID), slog.Error(err)) - continue - } - - // TODO Lazy-render the report. + for _, stats := range statsRows { + var failedBuilds []database.WorkspaceBuild reportData := map[string]any{} - templateAdmins, err := findTemplateAdmins(ctx, db, template) + if stats.FailedBuilds > 0 { + failedBuilds, err = db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ + TemplateID: stats.TemplateID, + Since: dbtime.Time(clk.Now()).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", template.ID), slog.Error(err)) + continue + } + + // TODO Lazy-render the report. + reportData = map[string]any{} + } + + templateAdmins, err := findTemplateAdmins(ctx, db, stats) if err != nil { - logger.Error(ctx, "unable to find template admins", slog.F("template_id", template.ID), slog.Error(err)) + logger.Error(ctx, "unable to find template admins", slog.F("template_id", stats.TemplateID), slog.Error(err)) continue } @@ -145,7 +147,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), }) if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) + logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) continue } } @@ -158,24 +160,24 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), }) if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) + logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) continue } } - templateDisplayName := template.DisplayName + templateDisplayName := stats.TemplateDisplayName if templateDisplayName == "" { - templateDisplayName = template.Name + templateDisplayName = stats.TemplateName } if _, err := enqueuer.EnqueueData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, map[string]string{ - "template_name": template.Name, + "template_name": stats.TemplateName, "template_display_name": templateDisplayName, }, reportData, "report_generator", - template.ID, template.OrganizationID, + stats.TemplateID, stats.TemplateOrganizationID, ); err != nil { logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err)) } @@ -186,7 +188,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), }) if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", template.ID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) + logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) continue } } @@ -206,7 +208,7 @@ func buildDataForReportFailedWorkspaceBuilds() map[string]any { return reportData } -func findTemplateAdmins(ctx context.Context, db database.Store, template database.Template) ([]database.GetUsersRow, error) { +func findTemplateAdmins(ctx context.Context, db database.Store, stats database.GetWorkspaceBuildStatsByTemplatesRow) ([]database.GetUsersRow, error) { users, err := db.GetUsers(ctx, database.GetUsersParams{ RbacRole: []string{codersdk.RoleTemplateAdmin}, }) @@ -229,7 +231,7 @@ func findTemplateAdmins(ctx context.Context, db database.Store, template databas } for _, entry := range orgIDsByMemberIDs { - if slices.Contains(entry.OrganizationIDs, template.OrganizationID) { + if slices.Contains(entry.OrganizationIDs, stats.TemplateOrganizationID) { templateAdmins = append(templateAdmins, usersByIDs[entry.UserID]) } } From 6eb67944afeb96ca646be9fbc5e95c79d90d8b5b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 13:26:54 +0000 Subject: [PATCH 029/122] WIP --- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 3559baab9a40d..de49c9c57e97d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -87,7 +87,7 @@ type sqlcQuerier interface { // A provisioner daemon with "zeroed" last_seen_at column indicates possible // connectivity issues (no provisioner daemon activity since registration). DeleteOldProvisionerDaemons(ctx context.Context) error - // Delete report generator logs that have been created at least a +5m ago. + // Delete report generator logs that have been created at least a +1h ago. DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error // If an agent hasn't connected in the last 7 days, we purge it's logs. // Exception: if the logs are related to the latest build, we keep those around. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index cb2621892f328..b5426bbe9070c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3461,10 +3461,10 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { } const deleteOldReportGeneratorLogs = `-- name: DeleteOldReportGeneratorLogs :exec -DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT($1::int, ' days')::interval - INTERVAL '5 min') +DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT($1::int, ' days')::interval - INTERVAL '1 hour') ` -// Delete report generator logs that have been created at least a +5m ago. +// Delete report generator logs that have been created at least a +1h ago. func (q *sqlQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, frequencyDays) return err From 0afbd565771d53f68e7206bbdb668e26338e66d8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 15:27:55 +0200 Subject: [PATCH 030/122] WIP --- coderd/database/dbauthz/dbauthz.go | 4 ---- coderd/database/dbmem/dbmem.go | 9 --------- coderd/database/dbmetrics/dbmetrics.go | 7 ------- 3 files changed, 20 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 6d9072dbf2b39..3dba531dbadcc 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -898,10 +898,6 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } -func (q *querier) GetWorkspaceBuildStats(ctx context.Context, arg database.GetWorkspaceBuildStatsParams) ([]database.GetWorkspaceBuildStatsRow, error) { - panic("not implemented") -} - func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 3192db0b1e68c..4e50e62382c0a 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -991,15 +991,6 @@ func (q *FakeQuerier) getOrganizationByIDNoLock(id uuid.UUID) (database.Organiza return database.Organization{}, sql.ErrNoRows } -func (q *FakeQuerier) GetWorkspaceBuildStats(ctx context.Context, arg database.GetWorkspaceBuildStatsParams) ([]database.GetWorkspaceBuildStatsRow, error) { - err := validateDatabaseType(arg) - if err != nil { - return nil, err - } - - panic("not implemented") -} - func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 98524d7326deb..602ca150b5fb1 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -81,13 +81,6 @@ func (m metricsStore) InTx(f func(database.Store) error, options *sql.TxOptions) return err } -func (m metricsStore) GetWorkspaceBuildStats(ctx context.Context, arg database.GetWorkspaceBuildStatsParams) ([]database.GetWorkspaceBuildStatsRow, error) { - start := time.Now() - r0, r1 := m.s.GetWorkspaceBuildStats(ctx, arg) - m.queryLatencies.WithLabelValues("GetWorkspaceBuildStats").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m metricsStore) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error { start := time.Now() err := m.s.AcquireLock(ctx, pgAdvisoryXactLock) From d0331af1803ca6736e3428859eb4f362a765c3e2 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 15:49:11 +0200 Subject: [PATCH 031/122] before input data --- coderd/notifications/reports/generator.go | 35 +++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 3bad00bd89621..509dd591c6bba 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -3,6 +3,7 @@ package reports import ( "context" "database/sql" + "fmt" "io" "slices" "sort" @@ -12,9 +13,10 @@ import ( "cdr.dev/slog" - "github.com/coder/quartz" "github.com/google/uuid" + "github.com/coder/quartz" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -114,12 +116,12 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat Since: dbtime.Time(clk.Now()).UTC(), }) if err != nil { - logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", template.ID), slog.Error(err)) + logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err)) continue } - // TODO Lazy-render the report. - reportData = map[string]any{} + // There are some failed builds, so we have to prepare input data for the report. + reportData = buildDataForReportFailedWorkspaceBuilds(frequencyDays, stats, failedBuilds) } templateAdmins, err := findTemplateAdmins(ctx, db, stats) @@ -129,8 +131,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { - // TODO Check if report is enabled for the person. - reportLog, err := db.GetReportGeneratorLogByUserAndTemplate(ctx, database.GetReportGeneratorLogByUserAndTemplateParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, @@ -201,10 +201,27 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return nil } -func buildDataForReportFailedWorkspaceBuilds() map[string]any { - // TODO Lazy-render the report. - reportData := map[string]any{} +func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.WorkspaceBuild) map[string]any { + // Format frequency label + var frequencyLabel string + if frequencyDays == 7 { + frequencyLabel = "week" + } else { + var plural string + if frequencyDays > 1 { + plural = "s" + } + frequencyLabel = fmt.Sprintf("%d day%s", frequencyDays, plural) + } + reportData := map[string]any{ + "failed_builds": stats.FailedBuilds, + "total_builds": stats.TotalBuilds, + "report_frequency": frequencyLabel, + "template_version": map[string]any{ + // TODO + }, + } return reportData } From 3d65010c044f17f81712d3e12eabeee393f3c167 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 17:22:16 +0200 Subject: [PATCH 032/122] Last TODO --- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbmem/dbmem.go | 2 +- coderd/database/dbmetrics/dbmetrics.go | 2 +- coderd/database/dbmock/dbmock.go | 4 +- coderd/database/models.go | 2 +- coderd/database/querier.go | 4 +- coderd/database/queries.sql.go | 55 +++++++++++++-------- coderd/database/queries/workspacebuilds.sql | 17 ++++++- coderd/notifications/reports/generator.go | 53 ++++++++++++++++---- 9 files changed, 101 insertions(+), 40 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 3dba531dbadcc..8ce0079d97fe9 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1434,7 +1434,7 @@ func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid. return fetchWithPostFilter(q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLinksByUserID)(ctx, userID) } -func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { +func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) { panic("not implemented") } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 4e50e62382c0a..4fa7cc57b5341 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -2511,7 +2511,7 @@ func (q *FakeQuerier) GetExternalAuthLinksByUserID(_ context.Context, userID uui return gals, nil } -func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { +func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 602ca150b5fb1..f352dbfcad82c 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -613,7 +613,7 @@ func (m metricsStore) GetExternalAuthLinksByUserID(ctx context.Context, userID u return r0, r1 } -func (m metricsStore) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { +func (m metricsStore) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) { start := time.Now() r0, r1 := m.s.GetFailedWorkspaceBuildsByTemplateID(ctx, arg) m.queryLatencies.WithLabelValues("GetFailedWorkspaceBuildsByTemplateID").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index bd16560cf4484..9f1f11c0a049f 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1209,10 +1209,10 @@ func (mr *MockStoreMockRecorder) GetExternalAuthLinksByUserID(arg0, arg1 any) *g } // GetFailedWorkspaceBuildsByTemplateID mocks base method. -func (m *MockStore) GetFailedWorkspaceBuildsByTemplateID(arg0 context.Context, arg1 database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.WorkspaceBuild, error) { +func (m *MockStore) GetFailedWorkspaceBuildsByTemplateID(arg0 context.Context, arg1 database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFailedWorkspaceBuildsByTemplateID", arg0, arg1) - ret0, _ := ret[0].([]database.WorkspaceBuild) + ret0, _ := ret[0].([]database.GetFailedWorkspaceBuildsByTemplateIDRow) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/coderd/database/models.go b/coderd/database/models.go index acd5c23c8879a..b2c5f851b7f78 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index de49c9c57e97d..02ebedca3c698 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -142,7 +142,7 @@ type sqlcQuerier interface { GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) - GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]WorkspaceBuild, error) + GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) // Get all templates that use a file. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b5426bbe9070c..3c093913c2a32 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -12526,17 +12526,33 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t const getFailedWorkspaceBuildsByTemplateID = `-- name: GetFailedWorkspaceBuildsByTemplateID :many SELECT - wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username + tv.name AS template_version_name, + u.username AS workspace_owner_username, + w.name AS workspace_name, + wb.build_number AS workspace_build_number, + pj.completed_at AS provisioner_job_completed_at FROM workspace_build_with_user AS wb JOIN workspaces AS w ON wb.workspace_id = w.id +JOIN + users AS u +ON + workspaces.owner_id = u.id JOIN provisioner_jobs AS pj ON wb.job_id = pj.id +JOIN + templates AS t +ON + w.template_id = t.id +JOIN + template_versions AS tv +ON + wb.template_version_id = tv.id WHERE w.template_id = $1 AND wb.created_at > $2 @@ -12549,32 +12565,29 @@ type GetFailedWorkspaceBuildsByTemplateIDParams struct { Since time.Time `db:"since" json:"since"` } -func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]WorkspaceBuild, error) { +type GetFailedWorkspaceBuildsByTemplateIDRow struct { + TemplateVersionName string `db:"template_version_name" json:"template_version_name"` + WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"` + WorkspaceName string `db:"workspace_name" json:"workspace_name"` + WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"` + ProvisionerJobCompletedAt sql.NullTime `db:"provisioner_job_completed_at" json:"provisioner_job_completed_at"` +} + +func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) { rows, err := q.db.QueryContext(ctx, getFailedWorkspaceBuildsByTemplateID, arg.TemplateID, arg.Since) if err != nil { return nil, err } defer rows.Close() - var items []WorkspaceBuild + var items []GetFailedWorkspaceBuildsByTemplateIDRow for rows.Next() { - var i WorkspaceBuild + var i GetFailedWorkspaceBuildsByTemplateIDRow if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.MaxDeadline, - &i.InitiatorByAvatarUrl, - &i.InitiatorByUsername, + &i.TemplateVersionName, + &i.WorkspaceOwnerUsername, + &i.WorkspaceName, + &i.WorkspaceBuildNumber, + &i.ProvisionerJobCompletedAt, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 17ef71ba09ce1..aa0b55656e49d 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -207,17 +207,32 @@ GROUP BY -- name: GetFailedWorkspaceBuildsByTemplateID :many SELECT - wb.* + tv.name AS template_version_name, + u.username AS workspace_owner_username, + w.name AS workspace_name, + wb.build_number AS workspace_build_number, FROM workspace_build_with_user AS wb JOIN workspaces AS w ON wb.workspace_id = w.id +JOIN + users AS u +ON + workspaces.owner_id = u.id JOIN provisioner_jobs AS pj ON wb.job_id = pj.id +JOIN + templates AS t +ON + w.template_id = t.id +JOIN + template_versions AS tv +ON + wb.template_version_id = tv.id WHERE w.template_id = $1 AND wb.created_at > @since diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 509dd591c6bba..1232b0d356967 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -107,7 +107,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, stats := range statsRows { - var failedBuilds []database.WorkspaceBuild + var failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow reportData := map[string]any{} if stats.FailedBuilds > 0 { @@ -201,7 +201,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return nil } -func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.WorkspaceBuild) map[string]any { +func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any { // Format frequency label var frequencyLabel string if frequencyDays == 7 { @@ -214,15 +214,48 @@ func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.G frequencyLabel = fmt.Sprintf("%d day%s", frequencyDays, plural) } - reportData := map[string]any{ - "failed_builds": stats.FailedBuilds, - "total_builds": stats.TotalBuilds, - "report_frequency": frequencyLabel, - "template_version": map[string]any{ - // TODO - }, + // Sorting order: template_version_name ASC, workspace build number DESC + sort.Slice(failedBuilds, func(i, j int) bool { + if failedBuilds[i].TemplateVersionName != failedBuilds[j].TemplateVersionName { + return failedBuilds[i].TemplateVersionName < failedBuilds[j].TemplateVersionName + } + return failedBuilds[i].WorkspaceBuildNumber > failedBuilds[j].WorkspaceBuildNumber + }) + + // Build notification model for template versions and failed workspace builds + templateVersions := []map[string]any{} + for _, failedBuild := range failedBuilds { + c := len(templateVersions) + + if len(templateVersions) == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName { + templateVersions = append(templateVersions, map[string]any{ + "template_version_name": failedBuild.TemplateVersionName, + "failed_count": 1, + "failed_builds": map[string]any{ + "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, + "workspace_name": failedBuild.WorkspaceName, + "build_number": failedBuild.WorkspaceBuildNumber, + }, + }) + continue + } + + //nolint:errorlint,forcetypeassert // only this function prepares the notification model + builds := templateVersions[c-1]["failed_builds"].([]map[string]any) + builds = append(builds, map[string]any{ + "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, + "workspace_name": failedBuild.WorkspaceName, + "build_number": failedBuild.WorkspaceBuildNumber, + }) + templateVersions[c-1]["failed_builds"] = builds + } + + return map[string]any{ + "failed_builds": stats.FailedBuilds, + "total_builds": stats.TotalBuilds, + "report_frequency": frequencyLabel, + "template_versions": templateVersions, } - return reportData } func findTemplateAdmins(ctx context.Context, db database.Store, stats database.GetWorkspaceBuildStatsByTemplatesRow) ([]database.GetUsersRow, error) { From 3490c57e53824e27800401a6b4fdc10a441b6462 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 17:23:09 +0200 Subject: [PATCH 033/122] WIP --- coderd/database/queries/workspacebuilds.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index aa0b55656e49d..d36123f7262c0 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -210,7 +210,7 @@ SELECT tv.name AS template_version_name, u.username AS workspace_owner_username, w.name AS workspace_name, - wb.build_number AS workspace_build_number, + wb.build_number AS workspace_build_number FROM workspace_build_with_user AS wb JOIN From aafed7ca60149e41bde6fc5edc8828274e65752f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 15:26:14 +0000 Subject: [PATCH 034/122] rebuild model --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 15 ++++++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index b2c5f851b7f78..acd5c23c8879a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 02ebedca3c698..ca8c2c90c7609 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3c093913c2a32..2c890f325f4ab 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database @@ -12529,8 +12529,7 @@ SELECT tv.name AS template_version_name, u.username AS workspace_owner_username, w.name AS workspace_name, - wb.build_number AS workspace_build_number, - pj.completed_at AS provisioner_job_completed_at + wb.build_number AS workspace_build_number FROM workspace_build_with_user AS wb JOIN @@ -12566,11 +12565,10 @@ type GetFailedWorkspaceBuildsByTemplateIDParams struct { } type GetFailedWorkspaceBuildsByTemplateIDRow struct { - TemplateVersionName string `db:"template_version_name" json:"template_version_name"` - WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"` - WorkspaceName string `db:"workspace_name" json:"workspace_name"` - WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"` - ProvisionerJobCompletedAt sql.NullTime `db:"provisioner_job_completed_at" json:"provisioner_job_completed_at"` + TemplateVersionName string `db:"template_version_name" json:"template_version_name"` + WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"` + WorkspaceName string `db:"workspace_name" json:"workspace_name"` + WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"` } func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) { @@ -12587,7 +12585,6 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a &i.WorkspaceOwnerUsername, &i.WorkspaceName, &i.WorkspaceBuildNumber, - &i.ProvisionerJobCompletedAt, ); err != nil { return nil, err } From 2d0299a7e018fcf19ec4c8aeb257c7976b5eda7a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 15:33:18 +0000 Subject: [PATCH 035/122] WIP --- ...down.sql => 000250_email_reports.down.sql} | 0 ...rts.up.sql => 000250_email_reports.up.sql} | 0 coderd/database/querier.go | 512 ------------------ 3 files changed, 512 deletions(-) rename coderd/database/migrations/{000249_email_reports.down.sql => 000250_email_reports.down.sql} (100%) rename coderd/database/migrations/{000249_email_reports.up.sql => 000250_email_reports.up.sql} (100%) delete mode 100644 coderd/database/querier.go diff --git a/coderd/database/migrations/000249_email_reports.down.sql b/coderd/database/migrations/000250_email_reports.down.sql similarity index 100% rename from coderd/database/migrations/000249_email_reports.down.sql rename to coderd/database/migrations/000250_email_reports.down.sql diff --git a/coderd/database/migrations/000249_email_reports.up.sql b/coderd/database/migrations/000250_email_reports.up.sql similarity index 100% rename from coderd/database/migrations/000249_email_reports.up.sql rename to coderd/database/migrations/000250_email_reports.up.sql diff --git a/coderd/database/querier.go b/coderd/database/querier.go deleted file mode 100644 index 331c6c36ef945..0000000000000 --- a/coderd/database/querier.go +++ /dev/null @@ -1,512 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 - -package database - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -type sqlcQuerier interface { - // Blocks until the lock is acquired. - // - // This must be called from within a transaction. The lock will be automatically - // released when the transaction ends. - AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error - // Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending. - // Only rows that aren't already leased (or ones which are leased but have exceeded their lease period) are returned. - // - // A "lease" here refers to a notifier taking ownership of a notification_messages row. A lease survives for the duration - // of CODER_NOTIFICATIONS_LEASE_PERIOD. Once a message is delivered, its status is updated and the lease expires (set to NULL). - // If a message exceeds its lease, that implies the notifier did not shutdown cleanly, or the table update failed somehow, - // and the row will then be eligible to be dequeued by another notifier. - // - // SKIP LOCKED is used to jump over locked rows. This prevents multiple notifiers from acquiring the same messages. - // See: https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE - // - AcquireNotificationMessages(ctx context.Context, arg AcquireNotificationMessagesParams) ([]AcquireNotificationMessagesRow, error) - // Acquires the lock for a single job that isn't started, completed, - // canceled, and that matches an array of provisioner types. - // - // SKIP LOCKED is used to jump over locked rows. This prevents - // multiple provisioners from acquiring the same jobs. See: - // https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE - AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) - // Bumps the workspace deadline by the template's configured "activity_bump" - // duration (default 1h). If the workspace bump will cross an autostart - // threshold, then the bump is autostart + TTL. This is the deadline behavior if - // the workspace was to autostart from a stopped state. - // - // Max deadline is respected, and the deadline will never be bumped past it. - // The deadline will never decrease. - // We only bump if the template has an activity bump duration set. - // We only bump if the raw interval is positive and non-zero. - // We only bump if workspace shutdown is manual. - // We only bump when 5% of the deadline has elapsed. - ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error - // AllUserIDs returns all UserIDs regardless of user status or deletion. - AllUserIDs(ctx context.Context) ([]uuid.UUID, error) - // Archiving templates is a soft delete action, so is reversible. - // Archiving prevents the version from being used and discovered - // by listing. - // Only unused template versions will be archived, which are any versions not - // referenced by the latest build of a workspace. - ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) - BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error - BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error) - BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error) - CleanTailnetCoordinators(ctx context.Context) error - CleanTailnetLostPeers(ctx context.Context) error - CleanTailnetTunnels(ctx context.Context) error - CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) - DeleteAPIKeyByID(ctx context.Context, id string) error - DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error - DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error - DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error - DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error - DeleteCoordinator(ctx context.Context, id uuid.UUID) error - DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error - DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error - DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error - DeleteGroupByID(ctx context.Context, id uuid.UUID) error - DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error - DeleteLicense(ctx context.Context, id int32) (int32, error) - DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error - DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error - DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error - DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error - DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error - // Delete all notification messages which have not been updated for over a week. - DeleteOldNotificationMessages(ctx context.Context) error - // Delete provisioner daemons that have been created at least a week ago - // and have not connected to coderd since a week. - // A provisioner daemon with "zeroed" last_seen_at column indicates possible - // connectivity issues (no provisioner daemon activity since registration). - DeleteOldProvisionerDaemons(ctx context.Context) error - // Delete report generator logs that have been created at least a +1h ago. - DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error - // If an agent hasn't connected in the last 7 days, we purge it's logs. - // Exception: if the logs are related to the latest build, we keep those around. - // Logs can take up a lot of space, so it's important we clean up frequently. - DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error - DeleteOldWorkspaceAgentStats(ctx context.Context) error - DeleteOrganization(ctx context.Context, id uuid.UUID) error - DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error - DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error - DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error - DeleteRuntimeConfig(ctx context.Context, key string) error - DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) - DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error) - DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error - DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) - DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) - DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error - DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error - EnqueueNotificationMessage(ctx context.Context, arg EnqueueNotificationMessageParams) error - FavoriteWorkspace(ctx context.Context, id uuid.UUID) error - // This is used to build up the notification_message's JSON payload. - FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error) - GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) - // there is no unique constraint on empty token names - GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) - GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) - GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) - GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) - GetActiveUserCount(ctx context.Context) (int64, error) - GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) - GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error) - // For PG Coordinator HTMLDebug - GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) - GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) - GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) - GetAnnouncementBanners(ctx context.Context) (string, error) - GetAppSecurityKey(ctx context.Context) (string, error) - GetApplicationName(ctx context.Context) (string, error) - // GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided - // ID. - GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) - // This function returns roles for authorization purposes. Implied member roles - // are included. - GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) - GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) - GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) - GetDERPMeshKey(ctx context.Context) (string, error) - GetDefaultOrganization(ctx context.Context) (Organization, error) - GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) - GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error) - GetDeploymentID(ctx context.Context) (string, error) - GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) - GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) - GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) - GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) - GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) - GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) - GetFileByID(ctx context.Context, id uuid.UUID) (File, error) - // Get all templates that use a file. - GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) - GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) - GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) - GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) - GetGroupMembers(ctx context.Context) ([]GroupMember, error) - GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]GroupMember, error) - // Returns the total count of members in a group. Shows the total - // count even if the caller does not have read access to ResourceGroupMember. - // They only need ResourceGroup read access. - GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) - GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) - GetHealthSettings(ctx context.Context) (string, error) - GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error) - GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) - GetLastUpdateCheck(ctx context.Context) (string, error) - GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) - GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) - GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) - GetLicenseByID(ctx context.Context, id int32) (License, error) - GetLicenses(ctx context.Context) ([]License, error) - GetLogoURL(ctx context.Context) (string, error) - GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) - GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) - GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) - GetNotificationsSettings(ctx context.Context) (string, error) - GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) - GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) - GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) - GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) - GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppSecret, error) - GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) - GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (OAuth2ProviderAppToken, error) - GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) - GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) - GetOAuthSigningKey(ctx context.Context) (string, error) - GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) - GetOrganizationByName(ctx context.Context, name string) (Organization, error) - GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) - GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) - GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error) - GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) - GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) - GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) - GetProvisionerDaemonsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerDaemon, error) - GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) - GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) - GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) - GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) - GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (ProvisionerKey, error) - GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) - GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) - GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) - GetQuotaAllowanceForUser(ctx context.Context, arg GetQuotaAllowanceForUserParams) (int64, error) - GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error) - GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) - GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) -<<<<<<< HEAD - // Fetch the report generator log indicating recent activity. - GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) -======= - GetRuntimeConfig(ctx context.Context, key string) (string, error) ->>>>>>> main - GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) - GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) - GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) - GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) - GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) - // GetTemplateAppInsights returns the aggregate usage of each app in a given - // timeframe. The result can be filtered on template_ids, meaning only user data - // from workspaces based on those templates will be included. - GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) - // GetTemplateAppInsightsByTemplate is used for Prometheus metrics. Keep - // in sync with GetTemplateAppInsights and UpsertTemplateUsageStats. - GetTemplateAppInsightsByTemplate(ctx context.Context, arg GetTemplateAppInsightsByTemplateParams) ([]GetTemplateAppInsightsByTemplateRow, error) - GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) - GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) - GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) - GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error) - // GetTemplateInsights returns the aggregate user-produced usage of all - // workspaces in a given timeframe. The template IDs, active users, and - // usage_seconds all reflect any usage in the template, including apps. - // - // When combining data from multiple templates, we must make a guess at - // how the user behaved for the 30 minute interval. In this case we make - // the assumption that if the user used two workspaces for 15 minutes, - // they did so sequentially, thus we sum the usage up to a maximum of - // 30 minutes with LEAST(SUM(n), 30). - GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) - // GetTemplateInsightsByInterval returns all intervals between start and end - // time, if end time is a partial interval, it will be included in the results and - // that interval will be shorter than a full one. If there is no data for a selected - // interval/template, it will be included in the results with 0 active users. - GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) - // GetTemplateInsightsByTemplate is used for Prometheus metrics. Keep - // in sync with GetTemplateInsights and UpsertTemplateUsageStats. - GetTemplateInsightsByTemplate(ctx context.Context, arg GetTemplateInsightsByTemplateParams) ([]GetTemplateInsightsByTemplateRow, error) - // GetTemplateParameterInsights does for each template in a given timeframe, - // look for the latest workspace build (for every workspace) that has been - // created in the timeframe and return the aggregate usage counts of parameter - // values. - GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) - GetTemplateUsageStats(ctx context.Context, arg GetTemplateUsageStatsParams) ([]TemplateUsageStat, error) - GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) - GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) - GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) - GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) - GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionVariable, error) - GetTemplateVersionWorkspaceTags(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionWorkspaceTag, error) - GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) - GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) - GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) - GetTemplates(ctx context.Context) ([]Template, error) - GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) - GetUnexpiredLicenses(ctx context.Context) ([]License, error) - // GetUserActivityInsights returns the ranking with top active users. - // The result can be filtered on template_ids, meaning only user data - // from workspaces based on those templates will be included. - // Note: The usage_seconds and usage_seconds_cumulative differ only when - // requesting deployment-wide (or multiple template) data. Cumulative - // produces a bloated value if a user has used multiple templates - // simultaneously. - GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) - GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) - GetUserByID(ctx context.Context, id uuid.UUID) (User, error) - GetUserCount(ctx context.Context) (int64, error) - // GetUserLatencyInsights returns the median and 95th percentile connection - // latency that users have experienced. The result can be filtered on - // template_ids, meaning only user data from workspaces based on those templates - // will be included. - GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) - GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error) - GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error) - GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error) - GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) - GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) - // This will never return deleted users. - GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) - // This shouldn't check for deleted, because it's frequently used - // to look up references to actions. eg. a user could build a workspace - // for another user, then be deleted... we still want them to appear! - GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) - GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) - GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) - GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) - GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) - GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) - GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) - GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) - GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) - GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) - GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) - GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error) - GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) - GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) - GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) - GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) - GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) - GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) - GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) - GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) - GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) - GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) - GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) - GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) - GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (GetWorkspaceByAgentIDRow, error) - GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) - GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) - GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) - GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) - // Finds a workspace proxy that has an access URL or app hostname that matches - // the provided hostname. This is to check if a hostname matches any workspace - // proxy. - // - // The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling - // this query. The scheme, port and path should be stripped. - // - GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) - GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) - GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) - GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) - GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) - GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) - GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) - GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) - GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) - GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) - // build_params is used to filter by build parameters if present. - // It has to be a CTE because the set returning function 'unnest' cannot - // be used in a WHERE clause. - GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) - GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) - InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) - // We use the organization_id as the id - // for simplicity since all users is - // every member of the org. - InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) - InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) - InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error) - InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error - InsertDERPMeshKey(ctx context.Context, value string) error - InsertDeploymentID(ctx context.Context, value string) error - InsertExternalAuthLink(ctx context.Context, arg InsertExternalAuthLinkParams) (ExternalAuthLink, error) - InsertFile(ctx context.Context, arg InsertFileParams) (File, error) - InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) - InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) - InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error - InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) - // Inserts any group by name that does not exist. All new groups are given - // a random uuid, are inserted into the same organization. They have the default - // values for avatar, display name, and quota allowance (all zero values). - // If the name conflicts, do nothing. - InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) - InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) - InsertOAuth2ProviderAppCode(ctx context.Context, arg InsertOAuth2ProviderAppCodeParams) (OAuth2ProviderAppCode, error) - InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) - InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) - InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) - InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) - InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) - InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) - InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) - InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) - InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) - InsertTemplate(ctx context.Context, arg InsertTemplateParams) error - InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error - InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) - InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) - InsertTemplateVersionWorkspaceTag(ctx context.Context, arg InsertTemplateVersionWorkspaceTagParams) (TemplateVersionWorkspaceTag, error) - InsertUser(ctx context.Context, arg InsertUserParams) (User, error) - // InsertUserGroupsByName adds a user to all provided groups, if they exist. - InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error - InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) - InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) - InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) - InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) - InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) - InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error - InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) - InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error - InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) - InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error - InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error - InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error - InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) - InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) - InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) - ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) - ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) - // Arguments are optional with uuid.Nil to ignore. - // - Use just 'organization_id' to get all members of an org - // - Use just 'user_id' to get all orgs a user is a member of - // - Use both to get a specific org member row - OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error) - ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error - RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) - RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error - RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error - // Non blocking lock. Returns true if the lock was acquired, false otherwise. - // - // This must be called from within a transaction. The lock will be automatically - // released when the transaction ends. - TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) - // This will always work regardless of the current state of the template version. - UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error - UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error - UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error - UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) - UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) - UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) - UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) - UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) - UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) - UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) - UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) - UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) - UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) - UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error - UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error - UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error - UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error - UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) - UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error - UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error - UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error - UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error - UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error - UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error - UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error - UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error - UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error - UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error - UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error - UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error) - UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error - UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error - UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error - UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) - UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) - UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) - UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) - UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error) - UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) - UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) - UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) - UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) - UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) - UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error - UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error - UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error - UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error - UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error - UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error - UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error - UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error - UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error - UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error - UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error - UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error - UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) - UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error - // This allows editing the properties of a workspace proxy. - UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) - UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error - UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error - UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]Workspace, error) - UpsertAnnouncementBanners(ctx context.Context, value string) error - UpsertAppSecurityKey(ctx context.Context, value string) error - UpsertApplicationName(ctx context.Context, value string) error - UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error - // The default proxy is implied and not actually stored in the database. - // So we need to store it's configuration here for display purposes. - // The functional values are immutable and controlled implicitly. - UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error - UpsertHealthSettings(ctx context.Context, value string) error - UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error - UpsertLastUpdateCheck(ctx context.Context, value string) error - UpsertLogoURL(ctx context.Context, value string) error - UpsertNotificationsSettings(ctx context.Context, value string) error - UpsertOAuthSigningKey(ctx context.Context, value string) error - UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) -<<<<<<< HEAD - // Insert or update report generator logs with recent activity. - UpsertReportGeneratorLog(ctx context.Context, arg UpsertReportGeneratorLogParams) error -======= - UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error ->>>>>>> main - UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) - UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) - UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error - UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) - UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) - UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) - // This query aggregates the workspace_agent_stats and workspace_app_stats data - // into a single table for efficient storage and querying. Half-hour buckets are - // used to store the data, and the minutes are summed for each user and template - // combination. The result is stored in the template_usage_stats table. - UpsertTemplateUsageStats(ctx context.Context) error - UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) -} - -var _ sqlcQuerier = (*sqlQuerier)(nil) From 38a3a508638fcf38950e97720f44bea545b8bbfc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 17:34:59 +0200 Subject: [PATCH 036/122] WIP --- coderd/database/dbauthz/dbauthz.go | 10 ++++------ coderd/database/dbmetrics/dbmetrics.go | 10 ++++------ coderd/database/dbmock/dbmock.go | 21 ++++++++------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9f39f71619a48..af1742e8cdf50 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1871,16 +1871,15 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti return q.db.GetReplicasUpdatedAfter(ctx, updatedAt) } -<<<<<<< HEAD func (q *querier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { panic("not implemented") -======= +} + func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err } return q.db.GetRuntimeConfig(ctx, key) ->>>>>>> main } func (q *querier) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) { @@ -3937,16 +3936,15 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse return q.db.UpsertProvisionerDaemon(ctx, arg) } -<<<<<<< HEAD func (q *querier) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { panic("not implemented") -======= +} + func (q *querier) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } return q.db.UpsertRuntimeConfig(ctx, arg) ->>>>>>> main } func (q *querier) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index da8385b108b11..6b404b8f8c426 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1012,17 +1012,16 @@ func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt tim return replicas, err } -<<<<<<< HEAD func (m metricsStore) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { start := time.Now() r0, r1 := m.s.GetReportGeneratorLogByUserAndTemplate(ctx, arg) m.queryLatencies.WithLabelValues("GetReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) -======= +} + func (m metricsStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) { start := time.Now() r0, r1 := m.s.GetRuntimeConfig(ctx, key) m.queryLatencies.WithLabelValues("GetRuntimeConfig").Observe(time.Since(start).Seconds()) ->>>>>>> main return r0, r1 } @@ -2496,17 +2495,16 @@ func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database. return r0, r1 } -<<<<<<< HEAD func (m metricsStore) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { start := time.Now() r0 := m.s.UpsertReportGeneratorLog(ctx, arg) m.queryLatencies.WithLabelValues("UpsertReportGeneratorLog").Observe(time.Since(start).Seconds()) -======= +} + func (m metricsStore) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { start := time.Now() r0 := m.s.UpsertRuntimeConfig(ctx, arg) m.queryLatencies.WithLabelValues("UpsertRuntimeConfig").Observe(time.Since(start).Seconds()) ->>>>>>> main return r0 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 047bf9b70bfe3..8534afe48bb1b 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2062,34 +2062,31 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1) } -<<<<<<< HEAD // GetReportGeneratorLogByUserAndTemplate mocks base method. func (m *MockStore) GetReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetReportGeneratorLogByUserAndTemplate", arg0, arg1) ret0, _ := ret[0].(database.ReportGeneratorLog) -======= +} + // GetRuntimeConfig mocks base method. func (m *MockStore) GetRuntimeConfig(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRuntimeConfig", arg0, arg1) ret0, _ := ret[0].(string) ->>>>>>> main ret1, _ := ret[1].(error) return ret0, ret1 } -<<<<<<< HEAD -// GetReportGeneratorLogByUserAndTemplate indicates an expected call of GetReportGeneratorLogByUserAndTemplate. func (mr *MockStoreMockRecorder) GetReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetReportGeneratorLogByUserAndTemplate), arg0, arg1) -======= +} + // GetRuntimeConfig indicates an expected call of GetRuntimeConfig. func (mr *MockStoreMockRecorder) GetRuntimeConfig(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRuntimeConfig", reflect.TypeOf((*MockStore)(nil).GetRuntimeConfig), arg0, arg1) ->>>>>>> main } // GetTailnetAgents mocks base method. @@ -5239,32 +5236,30 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) } -<<<<<<< HEAD // UpsertReportGeneratorLog mocks base method. func (m *MockStore) UpsertReportGeneratorLog(arg0 context.Context, arg1 database.UpsertReportGeneratorLogParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpsertReportGeneratorLog", arg0, arg1) -======= +} + // UpsertRuntimeConfig mocks base method. func (m *MockStore) UpsertRuntimeConfig(arg0 context.Context, arg1 database.UpsertRuntimeConfigParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpsertRuntimeConfig", arg0, arg1) ->>>>>>> main ret0, _ := ret[0].(error) return ret0 } -<<<<<<< HEAD // UpsertReportGeneratorLog indicates an expected call of UpsertReportGeneratorLog. func (mr *MockStoreMockRecorder) UpsertReportGeneratorLog(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertReportGeneratorLog), arg0, arg1) -======= +} + // UpsertRuntimeConfig indicates an expected call of UpsertRuntimeConfig. func (mr *MockStoreMockRecorder) UpsertRuntimeConfig(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRuntimeConfig", reflect.TypeOf((*MockStore)(nil).UpsertRuntimeConfig), arg0, arg1) ->>>>>>> main } // UpsertTailnetAgent mocks base method. From 4c6529534927e3c69cd4ffa223f6a7b09dca780b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 17:37:18 +0200 Subject: [PATCH 037/122] WIP --- coderd/database/dbmetrics/dbmetrics.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 6b404b8f8c426..221950f937acc 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1016,6 +1016,7 @@ func (m metricsStore) GetReportGeneratorLogByUserAndTemplate(ctx context.Context start := time.Now() r0, r1 := m.s.GetReportGeneratorLogByUserAndTemplate(ctx, arg) m.queryLatencies.WithLabelValues("GetReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) + return r0, r1 } func (m metricsStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) { From ef51c9ac7b22f5c9305c100cc9ab753534c26b93 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 17:37:57 +0200 Subject: [PATCH 038/122] WIP --- coderd/database/dbmetrics/dbmetrics.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 221950f937acc..84f2cf3be7115 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -2500,6 +2500,7 @@ func (m metricsStore) UpsertReportGeneratorLog(ctx context.Context, arg database start := time.Now() r0 := m.s.UpsertReportGeneratorLog(ctx, arg) m.queryLatencies.WithLabelValues("UpsertReportGeneratorLog").Observe(time.Since(start).Seconds()) + return r0 } func (m metricsStore) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { From 007bad67cbeb2a75e0a06adcd948f32e081e0b6c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Sep 2024 15:40:08 +0000 Subject: [PATCH 039/122] Fixed --- coderd/database/dbmock/dbmock.go | 27 +- coderd/database/querier.go | 506 +++++++++++++++++++++++++++++++ 2 files changed, 522 insertions(+), 11 deletions(-) create mode 100644 coderd/database/querier.go diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 8534afe48bb1b..5b1a093e56e0f 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2067,6 +2067,14 @@ func (m *MockStore) GetReportGeneratorLogByUserAndTemplate(arg0 context.Context, m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetReportGeneratorLogByUserAndTemplate", arg0, arg1) ret0, _ := ret[0].(database.ReportGeneratorLog) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetReportGeneratorLogByUserAndTemplate indicates an expected call of GetReportGeneratorLogByUserAndTemplate. +func (mr *MockStoreMockRecorder) GetReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetReportGeneratorLogByUserAndTemplate), arg0, arg1) } // GetRuntimeConfig mocks base method. @@ -2078,11 +2086,6 @@ func (m *MockStore) GetRuntimeConfig(arg0 context.Context, arg1 string) (string, return ret0, ret1 } -func (mr *MockStoreMockRecorder) GetReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetReportGeneratorLogByUserAndTemplate), arg0, arg1) -} - // GetRuntimeConfig indicates an expected call of GetRuntimeConfig. func (mr *MockStoreMockRecorder) GetRuntimeConfig(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() @@ -5240,12 +5243,6 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock func (m *MockStore) UpsertReportGeneratorLog(arg0 context.Context, arg1 database.UpsertReportGeneratorLogParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpsertReportGeneratorLog", arg0, arg1) -} - -// UpsertRuntimeConfig mocks base method. -func (m *MockStore) UpsertRuntimeConfig(arg0 context.Context, arg1 database.UpsertRuntimeConfigParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertRuntimeConfig", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } @@ -5256,6 +5253,14 @@ func (mr *MockStoreMockRecorder) UpsertReportGeneratorLog(arg0, arg1 any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertReportGeneratorLog), arg0, arg1) } +// UpsertRuntimeConfig mocks base method. +func (m *MockStore) UpsertRuntimeConfig(arg0 context.Context, arg1 database.UpsertRuntimeConfigParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertRuntimeConfig", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + // UpsertRuntimeConfig indicates an expected call of UpsertRuntimeConfig. func (mr *MockStoreMockRecorder) UpsertRuntimeConfig(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go new file mode 100644 index 0000000000000..16ca1c16b04a8 --- /dev/null +++ b/coderd/database/querier.go @@ -0,0 +1,506 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +type sqlcQuerier interface { + // Blocks until the lock is acquired. + // + // This must be called from within a transaction. The lock will be automatically + // released when the transaction ends. + AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error + // Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending. + // Only rows that aren't already leased (or ones which are leased but have exceeded their lease period) are returned. + // + // A "lease" here refers to a notifier taking ownership of a notification_messages row. A lease survives for the duration + // of CODER_NOTIFICATIONS_LEASE_PERIOD. Once a message is delivered, its status is updated and the lease expires (set to NULL). + // If a message exceeds its lease, that implies the notifier did not shutdown cleanly, or the table update failed somehow, + // and the row will then be eligible to be dequeued by another notifier. + // + // SKIP LOCKED is used to jump over locked rows. This prevents multiple notifiers from acquiring the same messages. + // See: https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE + // + AcquireNotificationMessages(ctx context.Context, arg AcquireNotificationMessagesParams) ([]AcquireNotificationMessagesRow, error) + // Acquires the lock for a single job that isn't started, completed, + // canceled, and that matches an array of provisioner types. + // + // SKIP LOCKED is used to jump over locked rows. This prevents + // multiple provisioners from acquiring the same jobs. See: + // https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE + AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) + // Bumps the workspace deadline by the template's configured "activity_bump" + // duration (default 1h). If the workspace bump will cross an autostart + // threshold, then the bump is autostart + TTL. This is the deadline behavior if + // the workspace was to autostart from a stopped state. + // + // Max deadline is respected, and the deadline will never be bumped past it. + // The deadline will never decrease. + // We only bump if the template has an activity bump duration set. + // We only bump if the raw interval is positive and non-zero. + // We only bump if workspace shutdown is manual. + // We only bump when 5% of the deadline has elapsed. + ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error + // AllUserIDs returns all UserIDs regardless of user status or deletion. + AllUserIDs(ctx context.Context) ([]uuid.UUID, error) + // Archiving templates is a soft delete action, so is reversible. + // Archiving prevents the version from being used and discovered + // by listing. + // Only unused template versions will be archived, which are any versions not + // referenced by the latest build of a workspace. + ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) + BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error + BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error) + BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error) + CleanTailnetCoordinators(ctx context.Context) error + CleanTailnetLostPeers(ctx context.Context) error + CleanTailnetTunnels(ctx context.Context) error + CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) + DeleteAPIKeyByID(ctx context.Context, id string) error + DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error + DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error + DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error + DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error + DeleteCoordinator(ctx context.Context, id uuid.UUID) error + DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error + DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error + DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error + DeleteGroupByID(ctx context.Context, id uuid.UUID) error + DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error + DeleteLicense(ctx context.Context, id int32) (int32, error) + DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error + DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error + DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error + DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error + DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error + // Delete all notification messages which have not been updated for over a week. + DeleteOldNotificationMessages(ctx context.Context) error + // Delete provisioner daemons that have been created at least a week ago + // and have not connected to coderd since a week. + // A provisioner daemon with "zeroed" last_seen_at column indicates possible + // connectivity issues (no provisioner daemon activity since registration). + DeleteOldProvisionerDaemons(ctx context.Context) error + // Delete report generator logs that have been created at least a +1h ago. + DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error + // If an agent hasn't connected in the last 7 days, we purge it's logs. + // Exception: if the logs are related to the latest build, we keep those around. + // Logs can take up a lot of space, so it's important we clean up frequently. + DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error + DeleteOldWorkspaceAgentStats(ctx context.Context) error + DeleteOrganization(ctx context.Context, id uuid.UUID) error + DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error + DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error + DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error + DeleteRuntimeConfig(ctx context.Context, key string) error + DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) + DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error) + DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error + DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) + DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) + DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error + DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error + EnqueueNotificationMessage(ctx context.Context, arg EnqueueNotificationMessageParams) error + FavoriteWorkspace(ctx context.Context, id uuid.UUID) error + // This is used to build up the notification_message's JSON payload. + FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error) + GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) + // there is no unique constraint on empty token names + GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) + GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) + GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) + GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) + GetActiveUserCount(ctx context.Context) (int64, error) + GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) + GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error) + // For PG Coordinator HTMLDebug + GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) + GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) + GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) + GetAnnouncementBanners(ctx context.Context) (string, error) + GetAppSecurityKey(ctx context.Context) (string, error) + GetApplicationName(ctx context.Context) (string, error) + // GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided + // ID. + GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) + // This function returns roles for authorization purposes. Implied member roles + // are included. + GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) + GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) + GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) + GetDERPMeshKey(ctx context.Context) (string, error) + GetDefaultOrganization(ctx context.Context) (Organization, error) + GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) + GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error) + GetDeploymentID(ctx context.Context) (string, error) + GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) + GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) + GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) + GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) + GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) + GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) + GetFileByID(ctx context.Context, id uuid.UUID) (File, error) + // Get all templates that use a file. + GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) + GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) + GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) + GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) + GetGroupMembers(ctx context.Context) ([]GroupMember, error) + GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]GroupMember, error) + // Returns the total count of members in a group. Shows the total + // count even if the caller does not have read access to ResourceGroupMember. + // They only need ResourceGroup read access. + GetGroupMembersCountByGroupID(ctx context.Context, groupID uuid.UUID) (int64, error) + GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) + GetHealthSettings(ctx context.Context) (string, error) + GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error) + GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) + GetLastUpdateCheck(ctx context.Context) (string, error) + GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) + GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) + GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) + GetLicenseByID(ctx context.Context, id int32) (License, error) + GetLicenses(ctx context.Context) ([]License, error) + GetLogoURL(ctx context.Context) (string, error) + GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) + GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) + GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) + GetNotificationsSettings(ctx context.Context) (string, error) + GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) + GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) + GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) + GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) + GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppSecret, error) + GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) + GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (OAuth2ProviderAppToken, error) + GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) + GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) + GetOAuthSigningKey(ctx context.Context) (string, error) + GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) + GetOrganizationByName(ctx context.Context, name string) (Organization, error) + GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) + GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) + GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error) + GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) + GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) + GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) + GetProvisionerDaemonsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerDaemon, error) + GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) + GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) + GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) + GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) + GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (ProvisionerKey, error) + GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) + GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) + GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) + GetQuotaAllowanceForUser(ctx context.Context, arg GetQuotaAllowanceForUserParams) (int64, error) + GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error) + GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) + GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) + // Fetch the report generator log indicating recent activity. + GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) + GetRuntimeConfig(ctx context.Context, key string) (string, error) + GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) + GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) + GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) + GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) + GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) + // GetTemplateAppInsights returns the aggregate usage of each app in a given + // timeframe. The result can be filtered on template_ids, meaning only user data + // from workspaces based on those templates will be included. + GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) + // GetTemplateAppInsightsByTemplate is used for Prometheus metrics. Keep + // in sync with GetTemplateAppInsights and UpsertTemplateUsageStats. + GetTemplateAppInsightsByTemplate(ctx context.Context, arg GetTemplateAppInsightsByTemplateParams) ([]GetTemplateAppInsightsByTemplateRow, error) + GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) + GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) + GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) + GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error) + // GetTemplateInsights returns the aggregate user-produced usage of all + // workspaces in a given timeframe. The template IDs, active users, and + // usage_seconds all reflect any usage in the template, including apps. + // + // When combining data from multiple templates, we must make a guess at + // how the user behaved for the 30 minute interval. In this case we make + // the assumption that if the user used two workspaces for 15 minutes, + // they did so sequentially, thus we sum the usage up to a maximum of + // 30 minutes with LEAST(SUM(n), 30). + GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) + // GetTemplateInsightsByInterval returns all intervals between start and end + // time, if end time is a partial interval, it will be included in the results and + // that interval will be shorter than a full one. If there is no data for a selected + // interval/template, it will be included in the results with 0 active users. + GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) + // GetTemplateInsightsByTemplate is used for Prometheus metrics. Keep + // in sync with GetTemplateInsights and UpsertTemplateUsageStats. + GetTemplateInsightsByTemplate(ctx context.Context, arg GetTemplateInsightsByTemplateParams) ([]GetTemplateInsightsByTemplateRow, error) + // GetTemplateParameterInsights does for each template in a given timeframe, + // look for the latest workspace build (for every workspace) that has been + // created in the timeframe and return the aggregate usage counts of parameter + // values. + GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) + GetTemplateUsageStats(ctx context.Context, arg GetTemplateUsageStatsParams) ([]TemplateUsageStat, error) + GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) + GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) + GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) + GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) + GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionVariable, error) + GetTemplateVersionWorkspaceTags(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionWorkspaceTag, error) + GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) + GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) + GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) + GetTemplates(ctx context.Context) ([]Template, error) + GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) + GetUnexpiredLicenses(ctx context.Context) ([]License, error) + // GetUserActivityInsights returns the ranking with top active users. + // The result can be filtered on template_ids, meaning only user data + // from workspaces based on those templates will be included. + // Note: The usage_seconds and usage_seconds_cumulative differ only when + // requesting deployment-wide (or multiple template) data. Cumulative + // produces a bloated value if a user has used multiple templates + // simultaneously. + GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) + GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) + GetUserByID(ctx context.Context, id uuid.UUID) (User, error) + GetUserCount(ctx context.Context) (int64, error) + // GetUserLatencyInsights returns the median and 95th percentile connection + // latency that users have experienced. The result can be filtered on + // template_ids, meaning only user data from workspaces based on those templates + // will be included. + GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) + GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error) + GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error) + GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error) + GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) + GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) + // This will never return deleted users. + GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) + // This shouldn't check for deleted, because it's frequently used + // to look up references to actions. eg. a user could build a workspace + // for another user, then be deleted... we still want them to appear! + GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) + GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) + GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) + GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) + GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) + GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) + GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) + GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) + GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) + GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) + GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) + GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error) + GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) + GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) + GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) + GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) + GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) + GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) + GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) + GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) + GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) + GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) + GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (GetWorkspaceByAgentIDRow, error) + GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) + GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) + GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) + GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) + // Finds a workspace proxy that has an access URL or app hostname that matches + // the provided hostname. This is to check if a hostname matches any workspace + // proxy. + // + // The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling + // this query. The scheme, port and path should be stripped. + // + GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) + GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) + GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) + GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) + GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) + GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) + GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) + GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) + GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) + GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) + // build_params is used to filter by build parameters if present. + // It has to be a CTE because the set returning function 'unnest' cannot + // be used in a WHERE clause. + GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) + GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) + InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) + // We use the organization_id as the id + // for simplicity since all users is + // every member of the org. + InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) + InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) + InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error) + InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error + InsertDERPMeshKey(ctx context.Context, value string) error + InsertDeploymentID(ctx context.Context, value string) error + InsertExternalAuthLink(ctx context.Context, arg InsertExternalAuthLinkParams) (ExternalAuthLink, error) + InsertFile(ctx context.Context, arg InsertFileParams) (File, error) + InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) + InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) + InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error + InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) + // Inserts any group by name that does not exist. All new groups are given + // a random uuid, are inserted into the same organization. They have the default + // values for avatar, display name, and quota allowance (all zero values). + // If the name conflicts, do nothing. + InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) + InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) + InsertOAuth2ProviderAppCode(ctx context.Context, arg InsertOAuth2ProviderAppCodeParams) (OAuth2ProviderAppCode, error) + InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) + InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) + InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) + InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) + InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) + InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) + InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) + InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) + InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) + InsertTemplate(ctx context.Context, arg InsertTemplateParams) error + InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error + InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) + InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) + InsertTemplateVersionWorkspaceTag(ctx context.Context, arg InsertTemplateVersionWorkspaceTagParams) (TemplateVersionWorkspaceTag, error) + InsertUser(ctx context.Context, arg InsertUserParams) (User, error) + // InsertUserGroupsByName adds a user to all provided groups, if they exist. + InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error + InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) + InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) + InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) + InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) + InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) + InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error + InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) + InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error + InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) + InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error + InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error + InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error + InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) + InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) + InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) + ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) + ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) + // Arguments are optional with uuid.Nil to ignore. + // - Use just 'organization_id' to get all members of an org + // - Use just 'user_id' to get all orgs a user is a member of + // - Use both to get a specific org member row + OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error) + ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error + RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) + RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error + RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error + // Non blocking lock. Returns true if the lock was acquired, false otherwise. + // + // This must be called from within a transaction. The lock will be automatically + // released when the transaction ends. + TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) + // This will always work regardless of the current state of the template version. + UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error + UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error + UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error + UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) + UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) + UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) + UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) + UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) + UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) + UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) + UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) + UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) + UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) + UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error + UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error + UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error + UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error + UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) + UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error + UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error + UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error + UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error + UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error + UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error + UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error + UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error + UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error + UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error + UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error + UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error) + UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error + UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error + UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error + UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) + UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) + UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) + UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) + UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error) + UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) + UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) + UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) + UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) + UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) + UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error + UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error + UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error + UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error + UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error + UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error + UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error + UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error + UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error + UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error + UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error + UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error + UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) + UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error + // This allows editing the properties of a workspace proxy. + UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) + UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error + UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error + UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]Workspace, error) + UpsertAnnouncementBanners(ctx context.Context, value string) error + UpsertAppSecurityKey(ctx context.Context, value string) error + UpsertApplicationName(ctx context.Context, value string) error + UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error + // The default proxy is implied and not actually stored in the database. + // So we need to store it's configuration here for display purposes. + // The functional values are immutable and controlled implicitly. + UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error + UpsertHealthSettings(ctx context.Context, value string) error + UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error + UpsertLastUpdateCheck(ctx context.Context, value string) error + UpsertLogoURL(ctx context.Context, value string) error + UpsertNotificationsSettings(ctx context.Context, value string) error + UpsertOAuthSigningKey(ctx context.Context, value string) error + UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) + // Insert or update report generator logs with recent activity. + UpsertReportGeneratorLog(ctx context.Context, arg UpsertReportGeneratorLogParams) error + UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error + UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) + UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) + UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error + UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) + UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) + UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) + // This query aggregates the workspace_agent_stats and workspace_app_stats data + // into a single table for efficient storage and querying. Half-hour buckets are + // used to store the data, and the minutes are summed for each user and template + // combination. The result is stored in the template_usage_stats table. + UpsertTemplateUsageStats(ctx context.Context) error + UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) +} + +var _ sqlcQuerier = (*sqlQuerier)(nil) From 73bb5bdf032572bb864cc6b3431c5070e7bbb991 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 09:19:24 +0200 Subject: [PATCH 040/122] dbauthz --- coderd/database/dbauthz/dbauthz.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index af1742e8cdf50..de10b3cadc642 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1145,7 +1145,10 @@ func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { } func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { + return err + } + return q.db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) } func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error { @@ -1442,7 +1445,10 @@ func (q *querier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid. } func (q *querier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg database.GetFailedWorkspaceBuildsByTemplateIDParams) ([]database.GetFailedWorkspaceBuildsByTemplateIDRow, error) { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetFailedWorkspaceBuildsByTemplateID(ctx, arg) } func (q *querier) GetFileByHashAndCreator(ctx context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { @@ -1872,7 +1878,10 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti } func (q *querier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.ReportGeneratorLog{}, err + } + return q.db.GetReportGeneratorLogByUserAndTemplate(ctx, arg) } func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) { @@ -2486,7 +2495,10 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil } func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetWorkspaceBuildStatsByTemplates(ctx, since) } func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { @@ -3937,7 +3949,10 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse } func (q *querier) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpsertReportGeneratorLog(ctx, arg) } func (q *querier) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { From 7a7bb85a931b5442e5a6da40cefbef51f2d14a71 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 09:41:08 +0200 Subject: [PATCH 041/122] fix --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 4 ++-- coderd/database/queries/notifications.sql | 4 ++-- coderd/database/queries/workspacebuilds.sql | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index e8124b0068b3c..1e172ca7e03a5 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 16ca1c16b04a8..1ccbd0079fc7e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5ba11ba0c40b8..ba240f3bc1831 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -12583,7 +12583,7 @@ ON JOIN users AS u ON - workspaces.owner_id = u.id + w.owner_id = u.id JOIN provisioner_jobs AS pj ON diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 61011502f1101..dff8ce0128836 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -186,8 +186,8 @@ WHERE -- name: UpsertReportGeneratorLog :exec -- Insert or update report generator logs with recent activity. -INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = $3 WHERE (user_id = $1 AND notification_template_id = $2); +INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = @last_generated_at WHERE (user_id = @user_id AND notification_template_id = @notification_template_id); -- name: DeleteOldReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a +1h ago. diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index d36123f7262c0..39776567da80c 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -220,7 +220,7 @@ ON JOIN users AS u ON - workspaces.owner_id = u.id + w.owner_id = u.id JOIN provisioner_jobs AS pj ON From e02d2714c65494a7125f04e57e63c5a11c309855 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 07:42:42 +0000 Subject: [PATCH 042/122] fix --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index 1e172ca7e03a5..e8124b0068b3c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 1ccbd0079fc7e..16ca1c16b04a8 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ba240f3bc1831..0d09836f57995 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database From 0b21a954ddf13bd8a2e379b71a0bf563333e33bf Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 09:50:18 +0200 Subject: [PATCH 043/122] excluded --- coderd/database/queries/notifications.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index dff8ce0128836..c4d5d5a8cf713 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -187,7 +187,7 @@ WHERE -- name: UpsertReportGeneratorLog :exec -- Insert or update report generator logs with recent activity. INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = @last_generated_at WHERE (user_id = @user_id AND notification_template_id = @notification_template_id); +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE (user_id = EXCLUDED.user_id AND notification_template_id = EXCLUDED.notification_template_id); -- name: DeleteOldReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a +1h ago. From 34c44b5954f2b8bf1dfc85425eb3ed22a57ca018 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 07:52:49 +0000 Subject: [PATCH 044/122] makegen --- coderd/database/queries.sql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0d09836f57995..66c83aa0cf96f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3778,7 +3778,7 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg const upsertReportGeneratorLog = `-- name: UpsertReportGeneratorLog :exec INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = $3 WHERE (user_id = $1 AND notification_template_id = $2) +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE (user_id = EXCLUDED.user_id AND notification_template_id = EXCLUDED.notification_template_id) ` type UpsertReportGeneratorLogParams struct { From f63a06b3cb8dec5aa8dd4ef1b3f5c7d186f11e01 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 09:57:46 +0200 Subject: [PATCH 045/122] wIP --- coderd/database/queries/notifications.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index c4d5d5a8cf713..c52c3b450f361 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -187,7 +187,7 @@ WHERE -- name: UpsertReportGeneratorLog :exec -- Insert or update report generator logs with recent activity. INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE (user_id = EXCLUDED.user_id AND notification_template_id = EXCLUDED.notification_template_id); +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at; -- name: DeleteOldReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a +1h ago. From 3b60a27f0e395ba774fa41a28e70d28fe8d020de Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 10:01:47 +0200 Subject: [PATCH 046/122] fix --- coderd/database/queries/notifications.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index c52c3b450f361..ff2bc10c01bcb 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -187,7 +187,8 @@ WHERE -- name: UpsertReportGeneratorLog :exec -- Insert or update report generator logs with recent activity. INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at; +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at +WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id); -- name: DeleteOldReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a +1h ago. From 774c4b0346734052d5d83fb4f8cfc0eca5c372cb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 10:02:07 +0200 Subject: [PATCH 047/122] fix --- coderd/database/queries/notifications.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index ff2bc10c01bcb..db89c96d20e64 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -188,7 +188,7 @@ WHERE -- Insert or update report generator logs with recent activity. INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id); +WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; -- name: DeleteOldReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a +1h ago. From 5d56c4d50d6a9c8d4aa9ed0733e5ed94274bcd26 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 08:15:47 +0000 Subject: [PATCH 048/122] makegen --- coderd/database/queries.sql.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 66c83aa0cf96f..5ea118fbd7ae7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3778,7 +3778,8 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg const upsertReportGeneratorLog = `-- name: UpsertReportGeneratorLog :exec INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE (user_id = EXCLUDED.user_id AND notification_template_id = EXCLUDED.notification_template_id) +ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at +WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id ` type UpsertReportGeneratorLogParams struct { From e8214e97700637b0e8fe96ddf8409bb393fdb41e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 12:09:56 +0200 Subject: [PATCH 049/122] dbmem --- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbmem/dbmem.go | 37 ++++++++++++++++++++--- coderd/database/dbmetrics/dbmetrics.go | 2 +- coderd/database/dbmock/dbmock.go | 2 +- coderd/database/models.go | 2 +- coderd/database/querier.go | 4 +-- coderd/database/queries.sql.go | 13 +++++--- coderd/database/queries/notifications.sql | 2 +- coderd/notifications/reports/generator.go | 5 ++- 9 files changed, 53 insertions(+), 16 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index de10b3cadc642..a69c5f3fb6686 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1144,7 +1144,7 @@ func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { return q.db.DeleteOldProvisionerDaemons(ctx) } -func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { +func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldReportGeneratorLogsParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9df3504029c3f..36b59e73fc5ce 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -156,6 +156,7 @@ type data struct { dbcryptKeys []database.DBCryptKey files []database.File externalAuthLinks []database.ExternalAuthLink + reportGeneratorLogs []database.ReportGeneratorLog gitSSHKey []database.GitSSHKey groupMembers []database.GroupMemberTable groups []database.Group @@ -1708,8 +1709,20 @@ func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { return nil } -func (q *FakeQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { - panic("not implemented") +func (q *FakeQuerier) DeleteOldReportGeneratorLogs(_ context.Context, params database.DeleteOldReportGeneratorLogsParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + now := dbtime.Now() + + var validLogs []database.ReportGeneratorLog + for _, record := range q.reportGeneratorLogs { + if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.Before(now.Add(-time.Duration(params.FrequencyDays)*24*time.Hour)) { + validLogs = append(validLogs, record) + } + } + q.reportGeneratorLogs = validLogs + return nil } func (q *FakeQuerier) DeleteOldWorkspaceAgentLogs(_ context.Context, threshold time.Time) error { @@ -9235,8 +9248,24 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up return d, nil } -func (q *FakeQuerier) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { - panic("not implemented") +func (q *FakeQuerier) UpsertReportGeneratorLog(_ context.Context, arg database.UpsertReportGeneratorLogParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, record := range q.reportGeneratorLogs { + if arg.NotificationTemplateID == record.NotificationTemplateID && arg.UserID == record.UserID { + q.reportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt + return nil + } + } + + q.reportGeneratorLogs = append(q.reportGeneratorLogs, database.ReportGeneratorLog(arg)) + return nil } func (q *FakeQuerier) UpsertRuntimeConfig(_ context.Context, arg database.UpsertRuntimeConfigParams) error { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 84f2cf3be7115..5a405d9b014f3 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -305,7 +305,7 @@ func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error { return r0 } -func (m metricsStore) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { +func (m metricsStore) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldReportGeneratorLogsParams) error { start := time.Now() r0 := m.s.DeleteOldReportGeneratorLogs(ctx, frequencyDays) m.queryLatencies.WithLabelValues("DeleteOldReportGeneratorLogs").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 5b1a093e56e0f..68cc2269a87f2 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -501,7 +501,7 @@ func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 any) *gomock.C } // DeleteOldReportGeneratorLogs mocks base method. -func (m *MockStore) DeleteOldReportGeneratorLogs(arg0 context.Context, arg1 int32) error { +func (m *MockStore) DeleteOldReportGeneratorLogs(arg0 context.Context, arg1 database.DeleteOldReportGeneratorLogsParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteOldReportGeneratorLogs", arg0, arg1) ret0, _ := ret[0].(error) diff --git a/coderd/database/models.go b/coderd/database/models.go index e8124b0068b3c..1e172ca7e03a5 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 16ca1c16b04a8..d0b41f07db4b5 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -88,7 +88,7 @@ type sqlcQuerier interface { // connectivity issues (no provisioner daemon activity since registration). DeleteOldProvisionerDaemons(ctx context.Context) error // Delete report generator logs that have been created at least a +1h ago. - DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error + DeleteOldReportGeneratorLogs(ctx context.Context, arg DeleteOldReportGeneratorLogsParams) error // If an agent hasn't connected in the last 7 days, we purge it's logs. // Exception: if the logs are related to the latest build, we keep those around. // Logs can take up a lot of space, so it's important we clean up frequently. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5ea118fbd7ae7..31dae3530e299 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -3461,12 +3461,17 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { } const deleteOldReportGeneratorLogs = `-- name: DeleteOldReportGeneratorLogs :exec -DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT($1::int, ' days')::interval - INTERVAL '1 hour') +DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT($1::int, ' days')::interval - INTERVAL '1 hour') AND notification_template_id = $2 ` +type DeleteOldReportGeneratorLogsParams struct { + FrequencyDays int32 `db:"frequency_days" json:"frequency_days"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` +} + // Delete report generator logs that have been created at least a +1h ago. -func (q *sqlQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays int32) error { - _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, frequencyDays) +func (q *sqlQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, arg DeleteOldReportGeneratorLogsParams) error { + _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, arg.FrequencyDays, arg.NotificationTemplateID) return err } diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index db89c96d20e64..ff28d190cf0c5 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -192,4 +192,4 @@ WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs -- name: DeleteOldReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a +1h ago. -DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT(@frequency_days::int, ' days')::interval - INTERVAL '1 hour'); +DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT(@frequency_days::int, ' days')::interval - INTERVAL '1 hour') AND notification_template_id = @notification_template_id; diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 1232b0d356967..e4dc65c311af5 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -194,7 +194,10 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } - err = db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) + err = db.DeleteOldReportGeneratorLogs(ctx, database.DeleteOldReportGeneratorLogsParams{ + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + FrequencyDays: frequencyDays, + }) if err != nil { return xerrors.Errorf("unable to delete old report generator logs: %w", err) } From 6ba2c29fca7408bfc50987d3fef408c9b14c8cba Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 10:12:11 +0000 Subject: [PATCH 050/122] makegen --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index 1e172ca7e03a5..e8124b0068b3c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d0b41f07db4b5..afe3d015da051 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 31dae3530e299..50387cf1b9d93 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database From 2962735e75bc3ac2762e80f4e0ce6234adf84312 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 13:41:49 +0200 Subject: [PATCH 051/122] last dbmem --- coderd/database/dbmem/dbmem.go | 117 ++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 36b59e73fc5ce..1035972ec1bca 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -2540,7 +2540,61 @@ func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, return nil, err } - panic("not implemented") + q.mutex.RLock() + defer q.mutex.RUnlock() + + workspaceBuildStats := []database.GetFailedWorkspaceBuildsByTemplateIDRow{} + for _, wb := range q.workspaceBuilds { + job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID) + if err != nil { + return nil, xerrors.Errorf("get provisioner job by ID: %w", err) + } + + if job.JobStatus != database.ProvisionerJobStatusFailed { + continue + } + + if !job.CompletedAt.Valid { + continue + } + + if wb.CreatedAt.Before(arg.Since) { + continue + } + + w, err := q.getWorkspaceByIDNoLock(ctx, wb.WorkspaceID) + if err != nil { + return nil, xerrors.Errorf("get workspace by ID: %w", err) + } + + t, err := q.getTemplateByIDNoLock(ctx, w.TemplateID) + if err != nil { + return nil, xerrors.Errorf("get template by ID: %w", err) + } + + if t.ID != arg.TemplateID { + continue + } + + workspaceOwner, err := q.getUserByIDNoLock(w.OwnerID) + if err != nil { + return nil, xerrors.Errorf("get user by ID: %w", err) + } + + templateVersion, err := q.getTemplateVersionByIDNoLock(ctx, wb.TemplateVersionID) + if err != nil { + return nil, xerrors.Errorf("get template version by ID: %w", err) + } + + workspaceBuildStats = append(workspaceBuildStats, database.GetFailedWorkspaceBuildsByTemplateIDRow{ + WorkspaceName: w.Name, + WorkspaceOwnerUsername: workspaceOwner.Username, + TemplateVersionName: templateVersion.Name, + WorkspaceBuildNumber: wb.BuildNumber, + }) + } + + return workspaceBuildStats, nil } func (q *FakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { @@ -3547,7 +3601,15 @@ func (q *FakeQuerier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context return database.ReportGeneratorLog{}, err } - panic("not implemented") + q.mutex.RLock() + q.mutex.RUnlock() + + for _, record := range q.reportGeneratorLogs { + if record.UserID == arg.UserID && record.NotificationTemplateID == arg.NotificationTemplateID { + return database.ReportGeneratorLog(record), nil + } + } + return database.ReportGeneratorLog{}, sql.ErrNoRows } func (q *FakeQuerier) GetRuntimeConfig(_ context.Context, key string) (string, error) { @@ -5850,7 +5912,56 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu } func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { - panic("not implemented") + q.mutex.RLock() + defer q.mutex.RUnlock() + + templateStats := map[uuid.UUID]database.GetWorkspaceBuildStatsByTemplatesRow{} + for _, wb := range q.workspaceBuilds { + job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID) + if err != nil { + return nil, xerrors.Errorf("get provisioner job by ID: %w", err) + } + + if !job.CompletedAt.Valid { + continue + } + + if wb.CreatedAt.Before(since) { + continue + } + + w, err := q.getWorkspaceByIDNoLock(ctx, wb.WorkspaceID) + if err != nil { + return nil, xerrors.Errorf("get workspace by ID: %w", err) + } + + if _, ok := templateStats[w.TemplateID]; !ok { + t, err := q.getTemplateByIDNoLock(ctx, w.TemplateID) + if err != nil { + return nil, xerrors.Errorf("get template by ID: %w", err) + } + + templateStats[w.TemplateID] = database.GetWorkspaceBuildStatsByTemplatesRow{ + TemplateID: w.TemplateID, + TemplateName: t.Name, + TemplateDisplayName: t.DisplayName, + TemplateOrganizationID: w.OrganizationID, + } + } + + s := templateStats[w.TemplateID] + s.TotalBuilds++ + if job.JobStatus == database.ProvisionerJobStatusFailed { + s.FailedBuilds++ + } + templateStats[w.TemplateID] = s + } + + rows := make([]database.GetWorkspaceBuildStatsByTemplatesRow, 0, len(templateStats)) + for _, ts := range templateStats { + rows = append(rows, ts) + } + return rows, nil } func (q *FakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, From d48a8ba80acf3ba7e346b3e99c1592da5cf9b192 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 13:50:28 +0200 Subject: [PATCH 052/122] fix --- coderd/database/dbmem/dbmem.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 1035972ec1bca..45b71569c763e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3595,18 +3595,18 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time. return replicas, nil } -func (q *FakeQuerier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (q *FakeQuerier) GetReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { err := validateDatabaseType(arg) if err != nil { return database.ReportGeneratorLog{}, err } q.mutex.RLock() - q.mutex.RUnlock() + defer q.mutex.RUnlock() for _, record := range q.reportGeneratorLogs { if record.UserID == arg.UserID && record.NotificationTemplateID == arg.NotificationTemplateID { - return database.ReportGeneratorLog(record), nil + return record, nil } } return database.ReportGeneratorLog{}, sql.ErrNoRows From 0bf474809e05f4f5a866bd4d381098bac7ff0b75 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 14:09:38 +0200 Subject: [PATCH 053/122] fix --- coderd/database/dbauthz/dbauthz.go | 4 ++-- coderd/database/dbauthz/dbauthz_test.go | 28 ++++++++++++++++++++++ coderd/database/migrations/migrate_test.go | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a69c5f3fb6686..c9aae78f2eee7 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1144,11 +1144,11 @@ func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { return q.db.DeleteOldProvisionerDaemons(ctx) } -func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldReportGeneratorLogsParams) error { +func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, arg database.DeleteOldReportGeneratorLogsParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } - return q.db.DeleteOldReportGeneratorLogs(ctx, frequencyDays) + return q.db.DeleteOldReportGeneratorLogs(ctx, arg) } func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index d23bb48184b61..56f40594a8d24 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2712,6 +2712,34 @@ func (s *MethodTestSuite) TestSystemFunctions() { Value: "value", }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) + s.Run("DeleteOldReportGeneratorLogs", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.DeleteOldReportGeneratorLogsParams{ + FrequencyDays: 1, + NotificationTemplateID: uuid.New(), + }).Asserts(rbac.ResourceSystem, policy.ActionDelete) + })) + s.Run("GetFailedWorkspaceBuildsByTemplateID", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.GetFailedWorkspaceBuildsByTemplateIDParams{ + TemplateID: uuid.New(), + Since: dbtime.Now(), + }).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetReportGeneratorLogByUserAndTemplate", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.GetReportGeneratorLogByUserAndTemplateParams{ + UserID: uuid.New(), + NotificationTemplateID: uuid.New(), + }).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) { + check.Args(1).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("UpsertReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.UpsertReportGeneratorLogParams{ + UserID: uuid.New(), + NotificationTemplateID: uuid.New(), + LastGeneratedAt: dbtime.Now(), + }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) } func (s *MethodTestSuite) TestNotifications() { diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index f7e284621edea..6bf28fecfb6db 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -268,6 +268,7 @@ func TestMigrateUpWithFixtures(t *testing.T) { "template_version_variables", "dbcrypt_keys", // having zero rows is a valid state for this table "template_version_workspace_tags", + "report_generator_logs", } s := &tableStats{s: make(map[string]int)} From 29d4a15687cdeeeceaf15442501e70c7b0ce5892 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 14:24:44 +0200 Subject: [PATCH 054/122] fix --- coderd/database/dbauthz/dbauthz_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 56f40594a8d24..974a72d71c48d 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2725,9 +2725,15 @@ func (s *MethodTestSuite) TestSystemFunctions() { }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetReportGeneratorLogByUserAndTemplate", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + _ = db.UpsertReportGeneratorLog(context.Background(), database.UpsertReportGeneratorLogParams{ + UserID: u.ID, + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + LastGeneratedAt: dbtime.Now(), + }) check.Args(database.GetReportGeneratorLogByUserAndTemplateParams{ UserID: uuid.New(), - NotificationTemplateID: uuid.New(), + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) { From a9580a50f70588219d3b9abe49707b766ab62b6b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 14:30:43 +0200 Subject: [PATCH 055/122] wip --- coderd/database/dbauthz/dbauthz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 974a72d71c48d..27a55f0a1f985 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2732,7 +2732,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { LastGeneratedAt: dbtime.Now(), }) check.Args(database.GetReportGeneratorLogByUserAndTemplateParams{ - UserID: uuid.New(), + UserID: u.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) From cf4609d6a4b43bdc9dd4e92127c21a6c6658a197 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 11 Sep 2024 14:38:55 +0200 Subject: [PATCH 056/122] fix --- coderd/database/dbauthz/dbauthz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 27a55f0a1f985..6803abc66341f 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2737,7 +2737,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) { - check.Args(1).Asserts(rbac.ResourceSystem, policy.ActionRead) + check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("UpsertReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) { check.Args(database.UpsertReportGeneratorLogParams{ From 8e8ab490866f13e9a77a98a4f3987b05fc182bfe Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 10:58:56 +0200 Subject: [PATCH 057/122] stub internal test --- coderd/notifications/reports/generator.go | 13 ++++--- .../reports/generator_internal_test.go | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 coderd/notifications/reports/generator_internal_test.go diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index e4dc65c311af5..714ad53679aed 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -28,7 +28,7 @@ const ( delay = 15 * time.Minute ) -func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, enqueur notifications.Enqueuer, clk quartz.Clock) io.Closer { +func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) io.Closer { closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) @@ -51,7 +51,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto return nil } - err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueur, clk) + err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueuer, clk) if err != nil { logger.Debug(ctx, "unable to report failed workspace builds") return err @@ -98,8 +98,9 @@ func (i *reportGenerator) Close() error { return nil } +const failedWorkspaceBuildsReportFrequencyDays = 7 + func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { - const frequencyDays = 7 statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(clk.Now()).UTC()) if err != nil { @@ -121,7 +122,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } // There are some failed builds, so we have to prepare input data for the report. - reportData = buildDataForReportFailedWorkspaceBuilds(frequencyDays, stats, failedBuilds) + reportData = buildDataForReportFailedWorkspaceBuilds(failedWorkspaceBuildsReportFrequencyDays, stats, failedBuilds) } templateAdmins, err := findTemplateAdmins(ctx, db, stats) @@ -139,7 +140,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return xerrors.Errorf("unable to get recent report generator log for user: %w", err) } - if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(frequencyDays*24*time.Hour).After(clk.Now()) { + if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequencyDays*24*time.Hour).After(clk.Now()) { // report generated recently, no need to send it now err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ UserID: templateAdmin.ID, @@ -196,7 +197,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat err = db.DeleteOldReportGeneratorLogs(ctx, database.DeleteOldReportGeneratorLogsParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - FrequencyDays: frequencyDays, + FrequencyDays: failedWorkspaceBuildsReportFrequencyDays, }) if err != nil { return xerrors.Errorf("unable to delete old report generator logs: %w", err) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go new file mode 100644 index 0000000000000..041b66a719ae2 --- /dev/null +++ b/coderd/notifications/reports/generator_internal_test.go @@ -0,0 +1,37 @@ +package reports + +import "testing" + +func TestReportFailedWorkspaceBuilds(t *testing.T) { + t.Parallel() + + t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report", func(t *testing.T) { + t.Parallel() + // TODO + }) + + t.Run("FailedBuilds_TemplateAdminOptIn_SecondRunTooEarly_NoReport", func(t *testing.T) { + t.Parallel() + // TODO + }) + + t.Run("FailedBuilds_TemplateAdminOptIn_SecondRun_Report", func(t *testing.T) { + t.Parallel() + // TODO + }) + + t.Run("NoFailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { + t.Parallel() + // TODO + }) + + t.Run("FailedBuilds_TemplateAdminOptOut_NoReport", func(t *testing.T) { + t.Parallel() + // TODO + }) + + t.Run("StaleFailedBuilds_TemplateAdminOptIn_NoReport_Cleanup", func(t *testing.T) { + t.Parallel() + // TODO + }) +} From afad1c128db9f768eb4f43ef001f25e2dad8f56d Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 11:21:32 +0200 Subject: [PATCH 058/122] WIP --- coderd/database/dbmem/dbmem.go | 4 +--- coderd/database/models.go | 2 +- coderd/database/querier.go | 4 ++-- coderd/database/queries.sql.go | 10 +++++----- coderd/database/queries/notifications.sql | 4 ++-- coderd/notifications/reports/generator.go | 2 +- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 45b71569c763e..d9ad810d81afa 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1713,11 +1713,9 @@ func (q *FakeQuerier) DeleteOldReportGeneratorLogs(_ context.Context, params dat q.mutex.Lock() defer q.mutex.Unlock() - now := dbtime.Now() - var validLogs []database.ReportGeneratorLog for _, record := range q.reportGeneratorLogs { - if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.Before(now.Add(-time.Duration(params.FrequencyDays)*24*time.Hour)) { + if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.Before(params.Before) { validLogs = append(validLogs, record) } } diff --git a/coderd/database/models.go b/coderd/database/models.go index e8124b0068b3c..c7f35eb543946 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index afe3d015da051..895b177bf86c8 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -87,7 +87,7 @@ type sqlcQuerier interface { // A provisioner daemon with "zeroed" last_seen_at column indicates possible // connectivity issues (no provisioner daemon activity since registration). DeleteOldProvisionerDaemons(ctx context.Context) error - // Delete report generator logs that have been created at least a +1h ago. + // Delete report generator logs that have been created at least a @before date. DeleteOldReportGeneratorLogs(ctx context.Context, arg DeleteOldReportGeneratorLogsParams) error // If an agent hasn't connected in the last 7 days, we purge it's logs. // Exception: if the logs are related to the latest build, we keep those around. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 50387cf1b9d93..2361eaf8f7e8e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -3461,17 +3461,17 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { } const deleteOldReportGeneratorLogs = `-- name: DeleteOldReportGeneratorLogs :exec -DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT($1::int, ' days')::interval - INTERVAL '1 hour') AND notification_template_id = $2 +DELETE FROM report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 ` type DeleteOldReportGeneratorLogsParams struct { - FrequencyDays int32 `db:"frequency_days" json:"frequency_days"` + Before time.Time `db:"before" json:"before"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` } -// Delete report generator logs that have been created at least a +1h ago. +// Delete report generator logs that have been created at least a @before date. func (q *sqlQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, arg DeleteOldReportGeneratorLogsParams) error { - _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, arg.FrequencyDays, arg.NotificationTemplateID) + _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, arg.Before, arg.NotificationTemplateID) return err } diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index ff28d190cf0c5..f4303f786398f 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -191,5 +191,5 @@ ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; -- name: DeleteOldReportGeneratorLogs :exec --- Delete report generator logs that have been created at least a +1h ago. -DELETE FROM report_generator_logs WHERE last_generated_at < (NOW() - CONCAT(@frequency_days::int, ' days')::interval - INTERVAL '1 hour') AND notification_template_id = @notification_template_id; +-- Delete report generator logs that have been created at least a @before date. +DELETE FROM report_generator_logs WHERE last_generated_at < @before::timestamptz AND notification_template_id = @notification_template_id; diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 714ad53679aed..8411e946b1aef 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -197,7 +197,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat err = db.DeleteOldReportGeneratorLogs(ctx, database.DeleteOldReportGeneratorLogsParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - FrequencyDays: failedWorkspaceBuildsReportFrequencyDays, + Before: dbtime.Time(clk.Now().Add(-failedWorkspaceBuildsReportFrequencyDays*24*time.Hour - time.Hour)).UTC(), }) if err != nil { return xerrors.Errorf("unable to delete old report generator logs: %w", err) From 8b49d552a4dfd6e454539e3dde0e1f056924e314 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 09:31:03 +0000 Subject: [PATCH 059/122] makegen --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index c7f35eb543946..e8124b0068b3c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 895b177bf86c8..092ca1c8ed295 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2361eaf8f7e8e..6d2a449581f26 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database From 87d40cbfb3691e14744b1f31bc1b21ce1d0a7f88 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 11:34:01 +0200 Subject: [PATCH 060/122] WIP --- coderd/notifications/reports/generator.go | 1 - .../notifications/reports/generator_internal_test.go | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 8411e946b1aef..de5dedae18553 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -101,7 +101,6 @@ func (i *reportGenerator) Close() error { const failedWorkspaceBuildsReportFrequencyDays = 7 func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { - statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(clk.Now()).UTC()) if err != nil { return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 041b66a719ae2..d07e887c94375 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -5,17 +5,7 @@ import "testing" func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() - t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report", func(t *testing.T) { - t.Parallel() - // TODO - }) - - t.Run("FailedBuilds_TemplateAdminOptIn_SecondRunTooEarly_NoReport", func(t *testing.T) { - t.Parallel() - // TODO - }) - - t.Run("FailedBuilds_TemplateAdminOptIn_SecondRun_Report", func(t *testing.T) { + t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { t.Parallel() // TODO }) From c352822d58e9822b6afa14a95b5b4a864cf55508 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 11:48:51 +0200 Subject: [PATCH 061/122] WIP: tests --- coderd/database/dbauthz/dbauthz_test.go | 2 +- .../reports/generator_internal_test.go | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6803abc66341f..a3e0a650bbc89 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2714,7 +2714,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { })) s.Run("DeleteOldReportGeneratorLogs", s.Subtest(func(db database.Store, check *expects) { check.Args(database.DeleteOldReportGeneratorLogsParams{ - FrequencyDays: 1, + Before: dbtime.Now(), NotificationTemplateID: uuid.New(), }).Asserts(rbac.ResourceSystem, policy.ActionDelete) })) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index d07e887c94375..55b27a059b647 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -1,12 +1,45 @@ package reports -import "testing" +import ( + "context" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { t.Parallel() + + // Prepare dependencies + logger := slogtest.Make(t, &slogtest.Options{}) + rdb, _ := dbtestutil.NewDB(t) + db := dbauthz.New(rdb, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) + notifyEnq := &testutil.FakeNotificationsEnqueuer{} + clk := quartz.NewMock(t) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() + + // Given + + // When + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifyEnq, clk) + require.NoError(t, err) + + // Then + // TODO }) From 22b14821fb83297f60d8231732ce83756deb159f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 11:56:52 +0200 Subject: [PATCH 062/122] cleanup --- .../reports/generator_internal_test.go | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 55b27a059b647..f0cd30cab2c59 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -7,13 +7,17 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/quartz" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/testutil" - "github.com/coder/quartz" ) func TestReportFailedWorkspaceBuilds(t *testing.T) { @@ -22,12 +26,8 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { t.Parallel() - // Prepare dependencies - logger := slogtest.Make(t, &slogtest.Options{}) - rdb, _ := dbtestutil.NewDB(t) - db := dbauthz.New(rdb, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) - notifyEnq := &testutil.FakeNotificationsEnqueuer{} - clk := quartz.NewMock(t) + // Setup + logger, db, notifEnq, clk := setup(t) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) defer cancel() @@ -35,7 +35,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Given // When - err := reportFailedWorkspaceBuilds(ctx, logger, db, notifyEnq, clk) + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) require.NoError(t, err) // Then @@ -58,3 +58,14 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // TODO }) } + +func setup(t *testing.T) (slog.Logger, database.Store, notifications.Enqueuer, quartz.Clock) { + t.Helper() + + logger := slogtest.Make(t, &slogtest.Options{}) + rdb, _ := dbtestutil.NewDB(t) + db := dbauthz.New(rdb, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) + notifyEnq := &testutil.FakeNotificationsEnqueuer{} + clk := quartz.NewMock(t) + return logger, db, notifyEnq, clk +} From 44b1bbafc1300bf345be904d177e759aab24c723 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 12:16:08 +0200 Subject: [PATCH 063/122] InitialState_NoBuilds_NoReport --- .../reports/generator_internal_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index f0cd30cab2c59..1f366566fef8d 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -23,23 +23,23 @@ import ( func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() - t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { + t.Run("InitialState_NoBuilds_NoReport", func(t *testing.T) { t.Parallel() // Setup logger, db, notifEnq, clk := setup(t) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) - defer cancel() - - // Given + // nolint:gocritic // reportFailedWorkspaceBuilds is called by system. + ctx := dbauthz.AsSystemRestricted(context.Background()) // When err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) - require.NoError(t, err) // Then + require.NoError(t, err) + }) + t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { + t.Parallel() // TODO }) From 121eef5e7e63f50fa7c83c9987cf40c236c38314 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 18:00:52 +0200 Subject: [PATCH 064/122] first semi test --- coderd/notifications/reports/generator.go | 17 ++- .../reports/generator_internal_test.go | 140 ++++++++++++++++-- 2 files changed, 141 insertions(+), 16 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index de5dedae18553..999cdb06208b2 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -101,10 +101,13 @@ func (i *reportGenerator) Close() error { const failedWorkspaceBuildsReportFrequencyDays = 7 func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { - statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(clk.Now()).UTC()) + statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(clk.Now().Add(-failedWorkspaceBuildsReportFrequencyDays*24*time.Hour)).UTC()) if err != nil { return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) } + sort.Slice(statsRows, func(i, j int) bool { + return statsRows[i].TemplateName < statsRows[j].TemplateName + }) for _, stats := range statsRows { var failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow @@ -161,8 +164,8 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat }) if err != nil { logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) - continue } + continue } templateDisplayName := stats.TemplateDisplayName @@ -234,10 +237,12 @@ func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.G templateVersions = append(templateVersions, map[string]any{ "template_version_name": failedBuild.TemplateVersionName, "failed_count": 1, - "failed_builds": map[string]any{ - "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, - "workspace_name": failedBuild.WorkspaceName, - "build_number": failedBuild.WorkspaceBuildNumber, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, + "workspace_name": failedBuild.WorkspaceName, + "build_number": failedBuild.WorkspaceBuildNumber, + }, }, }) continue diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 1f366566fef8d..8eb20544f2e46 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -2,24 +2,34 @@ package reports import ( "context" + "database/sql" "testing" + "time" - "github.com/prometheus/client_golang/prometheus" + "github.com/google/uuid" "github.com/stretchr/testify/require" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/quartz" - "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/testutil" ) +const dayDuration = 24 * time.Hour + +var ( + jobError = sql.NullString{String: "badness", Valid: true} + jobErrorCode = sql.NullString{String: "ERR-42", Valid: true} +) + func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() @@ -27,9 +37,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() // Setup - logger, db, notifEnq, clk := setup(t) - // nolint:gocritic // reportFailedWorkspaceBuilds is called by system. - ctx := dbauthz.AsSystemRestricted(context.Background()) + ctx, logger, db, _, notifEnq, clk := setup(t) // When err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) @@ -40,7 +48,116 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { t.Parallel() - // TODO + + // Setup + ctx, logger, db, ps, notifEnq, clk := setup(t) + + // Given + // Organization + org := dbgen.Organization(t, db, database.Organization{}) + + // Template admins + templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID}) + templateAdmin2 := dbgen.User(t, db, database.User{Username: "template-admin-2", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin2.ID, OrganizationID: org.ID}) + _ = dbgen.User(t, db, database.User{Name: "template-admin-3", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + // template admin in some other org + + // Regular users + user1 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) + user2 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user2.ID, OrganizationID: org.ID}) + user3 := dbgen.User(t, db, database.User{}) + // user in some other org + + // Templates + t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", OrganizationID: org.ID}) + t2 := dbgen.Template(t, db, database.Template{Name: "template-2", OrganizationID: org.ID}) + + // Template versions + t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + t1v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-2", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + t2v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-2-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) + t2v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) + + // Workspaces + w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) + w2 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t2.ID, OwnerID: user2.ID, OrganizationID: org.ID}) + w3 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user3.ID, OrganizationID: org.ID}) + w4 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t2.ID, OwnerID: user2.ID, OrganizationID: org.ID}) + + now := clk.Now() + + // Workspace builds + w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, TemplateVersionID: t1v2.ID, JobID: w1wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w1wb3pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-4 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, TemplateVersionID: t1v2.ID, JobID: w1wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + w2wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, TemplateVersionID: t2v1.ID, JobID: w2wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w2wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-4 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, TemplateVersionID: t2v2.ID, JobID: w2wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w2wb3pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-3 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, TemplateVersionID: t2v2.ID, JobID: w2wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + w3wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-3 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w3.ID, TemplateVersionID: t1v1.ID, JobID: w3wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + w4wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, TemplateVersionID: t2v1.ID, JobID: w4wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w4wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-1 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, TemplateVersionID: t2v2.ID, JobID: w4wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + // Database is ready, so we can clear notifications queue + notifEnq.Clear() + + // When + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then + require.NoError(t, err) + + require.Len(t, notifEnq.Sent, 4) // 2 templates, 2 template admins + require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin1.ID) + require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[0].Labels["template_name"], t1.Name) + require.Equal(t, notifEnq.Sent[0].Labels["template_display_name"], t1.DisplayName) + require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(3)) + require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(4)) + require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") + + require.Equal(t, notifEnq.Sent[1].UserID, templateAdmin2.ID) + require.Equal(t, notifEnq.Sent[1].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[1].Labels["template_name"], t1.Name) + require.Equal(t, notifEnq.Sent[1].Labels["template_display_name"], t1.DisplayName) + require.Equal(t, notifEnq.Sent[1].Data["failed_builds"], int64(3)) + require.Equal(t, notifEnq.Sent[1].Data["total_builds"], int64(4)) + require.Equal(t, notifEnq.Sent[1].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[1].Data["template_versions"], "?") + + require.Equal(t, notifEnq.Sent[2].UserID, templateAdmin1.ID) + require.Equal(t, notifEnq.Sent[2].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[2].Labels["template_name"], t2.Name) + require.Equal(t, notifEnq.Sent[2].Labels["template_display_name"], t2.DisplayName) + require.Equal(t, notifEnq.Sent[2].Data["failed_builds"], int64(3)) + require.Equal(t, notifEnq.Sent[2].Data["total_builds"], int64(5)) + require.Equal(t, notifEnq.Sent[2].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") + + require.Equal(t, notifEnq.Sent[3].UserID, templateAdmin2.ID) + require.Equal(t, notifEnq.Sent[3].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[3].Labels["template_name"], t2.Name) + require.Equal(t, notifEnq.Sent[3].Labels["template_display_name"], t2.DisplayName) + require.Equal(t, notifEnq.Sent[3].Data["failed_builds"], int64(3)) + require.Equal(t, notifEnq.Sent[3].Data["total_builds"], int64(5)) + require.Equal(t, notifEnq.Sent[3].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") }) t.Run("NoFailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { @@ -59,13 +176,16 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { }) } -func setup(t *testing.T) (slog.Logger, database.Store, notifications.Enqueuer, quartz.Clock) { +func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.Pubsub, *testutil.FakeNotificationsEnqueuer, quartz.Clock) { t.Helper() + // nolint:gocritic // reportFailedWorkspaceBuilds is called by system. + ctx := dbauthz.AsSystemRestricted(context.Background()) logger := slogtest.Make(t, &slogtest.Options{}) - rdb, _ := dbtestutil.NewDB(t) - db := dbauthz.New(rdb, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) + db, ps := dbtestutil.NewDB(t) + // does not work with works + // db := dbauthz.New(rdb, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) notifyEnq := &testutil.FakeNotificationsEnqueuer{} clk := quartz.NewMock(t) - return logger, db, notifyEnq, clk + return ctx, logger, db, ps, notifyEnq, clk } From bc92fc4af7e74f20f00912ed11b24daee5a08df3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 18:09:36 +0200 Subject: [PATCH 065/122] fix: created_by --- coderd/notifications/reports/generator_internal_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 8eb20544f2e46..7c0d99dad849f 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -73,8 +73,8 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // user in some other org // Templates - t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", OrganizationID: org.ID}) - t2 := dbgen.Template(t, db, database.Template{Name: "template-2", OrganizationID: org.ID}) + t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) + t2 := dbgen.Template(t, db, database.Template{Name: "template-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) // Template versions t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) From ccc680388a62ae18dda84dd11c8b23f7b6a1ea2c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 18:27:24 +0200 Subject: [PATCH 066/122] fix: created_by tv --- coderd/notifications/reports/generator_internal_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 7c0d99dad849f..cb1d97bc6f83c 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -77,10 +77,10 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t2 := dbgen.Template(t, db, database.Template{Name: "template-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) // Template versions - t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) - t1v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-2", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) - t2v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-2-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) - t2v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) + t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + t1v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + t2v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-2-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) + t2v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) // Workspaces w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) From f7826d6d32427e2c54a81835317fe9ade36f5497 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Sep 2024 18:36:24 +0200 Subject: [PATCH 067/122] fix: build number --- .../reports/generator_internal_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index cb1d97bc6f83c..f76636a35e70d 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -92,26 +92,26 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Workspace builds w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, TemplateVersionID: t1v2.ID, JobID: w1wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v2.ID, JobID: w1wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w1wb3pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-4 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, TemplateVersionID: t1v2.ID, JobID: w1wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 3, TemplateVersionID: t1v2.ID, JobID: w1wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w2wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, TemplateVersionID: t2v1.ID, JobID: w2wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 4, TemplateVersionID: t2v1.ID, JobID: w2wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w2wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-4 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, TemplateVersionID: t2v2.ID, JobID: w2wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 5, TemplateVersionID: t2v2.ID, JobID: w2wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w2wb3pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-3 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, TemplateVersionID: t2v2.ID, JobID: w2wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 6, TemplateVersionID: t2v2.ID, JobID: w2wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w3wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-3 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w3.ID, TemplateVersionID: t1v1.ID, JobID: w3wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w3.ID, BuildNumber: 7, TemplateVersionID: t1v1.ID, JobID: w3wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w4wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, TemplateVersionID: t2v1.ID, JobID: w4wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 8, TemplateVersionID: t2v1.ID, JobID: w4wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w4wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-1 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, TemplateVersionID: t2v2.ID, JobID: w4wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 9, TemplateVersionID: t2v2.ID, JobID: w4wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) // Database is ready, so we can clear notifications queue notifEnq.Clear() From 36644b217065bec6a68700a1bc84c3de43ec1441 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 10:13:50 +0200 Subject: [PATCH 068/122] expanding --- coderd/database/dbmem/dbmem.go | 2 +- coderd/notifications/reports/generator.go | 20 ++++++++------ .../reports/generator_internal_test.go | 27 ++++++++++++++++--- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d9ad810d81afa..c74bca9411d57 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1715,7 +1715,7 @@ func (q *FakeQuerier) DeleteOldReportGeneratorLogs(_ context.Context, params dat var validLogs []database.ReportGeneratorLog for _, record := range q.reportGeneratorLogs { - if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.Before(params.Before) { + if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.After(params.Before) { validLogs = append(validLogs, record) } } diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 999cdb06208b2..cf8eca2847551 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -101,7 +101,11 @@ func (i *reportGenerator) Close() error { const failedWorkspaceBuildsReportFrequencyDays = 7 func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { - statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(clk.Now().Add(-failedWorkspaceBuildsReportFrequencyDays*24*time.Hour)).UTC()) + now := clk.Now() + since := now.Add(-failedWorkspaceBuildsReportFrequencyDays * 24 * time.Hour) + + // TODO skip new templates + statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(since).UTC()) if err != nil { return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) } @@ -116,7 +120,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat if stats.FailedBuilds > 0 { failedBuilds, err = db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ TemplateID: stats.TemplateID, - Since: dbtime.Time(clk.Now()).UTC(), + Since: dbtime.Time(now).UTC(), }) if err != nil { logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err)) @@ -142,17 +146,17 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return xerrors.Errorf("unable to get recent report generator log for user: %w", err) } - if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequencyDays*24*time.Hour).After(clk.Now()) { + if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequencyDays*24*time.Hour).After(now) { // report generated recently, no need to send it now err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), + LastGeneratedAt: dbtime.Time(now).UTC(), }) if err != nil { logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) - continue } + continue } if len(failedBuilds) == 0 { @@ -160,7 +164,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), + LastGeneratedAt: dbtime.Time(now).UTC(), }) if err != nil { logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) @@ -188,7 +192,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(clk.Now()).UTC(), + LastGeneratedAt: dbtime.Time(now).UTC(), }) if err != nil { logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) @@ -199,7 +203,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat err = db.DeleteOldReportGeneratorLogs(ctx, database.DeleteOldReportGeneratorLogsParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - Before: dbtime.Time(clk.Now().Add(-failedWorkspaceBuildsReportFrequencyDays*24*time.Hour - time.Hour)).UTC(), + Before: dbtime.Time(now.Add(-failedWorkspaceBuildsReportFrequencyDays*24*time.Hour - time.Hour)).UTC(), }) if err != nil { return xerrors.Errorf("unable to delete old report generator logs: %w", err) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index f76636a35e70d..170131130f21f 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -7,12 +7,14 @@ import ( "time" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/quartz" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" @@ -39,11 +41,15 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Setup ctx, logger, db, _, notifEnq, clk := setup(t) + // Database is ready, so we can clear notifications queue + notifEnq.Clear() + // When err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) // Then require.NoError(t, err) + require.Empty(t, notifEnq.Sent) }) t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { @@ -117,7 +123,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { notifEnq.Clear() // When - err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) // Then require.NoError(t, err) @@ -158,6 +164,17 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Equal(t, notifEnq.Sent[3].Data["total_builds"], int64(5)) require.Equal(t, notifEnq.Sent[3].Data["report_frequency"], "week") // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") + + // Given: 6 days later (less than report frequency) + clk.Advance(6 * dayDuration).MustWait(context.Background()) + notifEnq.Clear() + + // When + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + require.NoError(t, err) + + // Then + require.Empty(t, notifEnq.Sent) // no notifications as it is too early. }) t.Run("NoFailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { @@ -176,16 +193,18 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { }) } -func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.Pubsub, *testutil.FakeNotificationsEnqueuer, quartz.Clock) { +func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.Pubsub, *testutil.FakeNotificationsEnqueuer, *quartz.Mock) { t.Helper() // nolint:gocritic // reportFailedWorkspaceBuilds is called by system. ctx := dbauthz.AsSystemRestricted(context.Background()) logger := slogtest.Make(t, &slogtest.Options{}) db, ps := dbtestutil.NewDB(t) - // does not work with works - // db := dbauthz.New(rdb, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) notifyEnq := &testutil.FakeNotificationsEnqueuer{} clk := quartz.NewMock(t) return ctx, logger, db, ps, notifyEnq, clk } + +func authedDB(db database.Store, logger slog.Logger) database.Store { + return dbauthz.New(db, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) +} From 52aafacdd3097db49bd1f6739c5a70d6d0a54a56 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 10:31:07 +0200 Subject: [PATCH 069/122] fix: snapshot --- coderd/notifications/reports/generator.go | 42 +++++++------------ .../reports/generator_internal_test.go | 5 +++ 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index cf8eca2847551..9b27af14035ba 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -113,6 +113,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return statsRows[i].TemplateName < statsRows[j].TemplateName }) + reportRecipients := map[uuid.UUID]bool{} for _, stats := range statsRows { var failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow reportData := map[string]any{} @@ -143,32 +144,19 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { // sql.ErrNoRows: report not generated yet - return xerrors.Errorf("unable to get recent report generator log for user: %w", err) + logger.Error(ctx, "unable to get recent report generator log for user", slog.F("user_id", templateAdmin.ID), slog.Error(err)) + continue } if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequencyDays*24*time.Hour).After(now) { // report generated recently, no need to send it now - err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ - UserID: templateAdmin.ID, - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(now).UTC(), - }) - if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) - } + reportRecipients[templateAdmin.ID] = true continue } if len(failedBuilds) == 0 { // no failed workspace builds, no need to send the report - err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ - UserID: templateAdmin.ID, - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(now).UTC(), - }) - if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) - } + reportRecipients[templateAdmin.ID] = true continue } @@ -188,16 +176,18 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat ); err != nil { logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err)) } + reportRecipients[templateAdmin.ID] = true + } + } - err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ - UserID: templateAdmin.ID, - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(now).UTC(), - }) - if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("template_id", stats.TemplateID), slog.F("user_id", templateAdmin.ID), slog.F("failed_builds", len(failedBuilds)), slog.Error(err)) - continue - } + for recipient := range reportRecipients { + err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ + UserID: recipient, + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + LastGeneratedAt: dbtime.Time(now).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to update report generator logs", slog.F("user_id", recipient), slog.Error(err)) } } diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 170131130f21f..58f6e817b68e2 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -191,6 +191,11 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() // TODO }) + + t.Run("FreshTemplate_FailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { + t.Parallel() + // TODO + }) } func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.Pubsub, *testutil.FakeNotificationsEnqueuer, *quartz.Mock) { From 8909bb6ce9ff2f474a2f2c4daa2115bc28a24364 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 11:29:08 +0200 Subject: [PATCH 070/122] test done --- coderd/notifications/reports/generator.go | 11 ++-- .../reports/generator_internal_test.go | 61 ++++++++++++++----- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 9b27af14035ba..9e6e9ff562b00 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -113,7 +113,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return statsRows[i].TemplateName < statsRows[j].TemplateName }) - reportRecipients := map[uuid.UUID]bool{} + reportGeneratedNow := map[uuid.UUID]bool{} for _, stats := range statsRows { var failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow reportData := map[string]any{} @@ -121,7 +121,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat if stats.FailedBuilds > 0 { failedBuilds, err = db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ TemplateID: stats.TemplateID, - Since: dbtime.Time(now).UTC(), + Since: dbtime.Time(since).UTC(), }) if err != nil { logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err)) @@ -150,13 +150,13 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequencyDays*24*time.Hour).After(now) { // report generated recently, no need to send it now - reportRecipients[templateAdmin.ID] = true continue } + reportGeneratedNow[templateAdmin.ID] = true + if len(failedBuilds) == 0 { // no failed workspace builds, no need to send the report - reportRecipients[templateAdmin.ID] = true continue } @@ -176,11 +176,10 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat ); err != nil { logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err)) } - reportRecipients[templateAdmin.ID] = true } } - for recipient := range reportRecipients { + for recipient := range reportGeneratedNow { err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ UserID: recipient, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 58f6e817b68e2..e956c18b44877 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -98,26 +98,26 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Workspace builds w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-6 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v2.ID, JobID: w1wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v2.ID, JobID: w1wb2pj.ID, CreatedAt: now.Add(-5 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w1wb3pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-4 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 3, TemplateVersionID: t1v2.ID, JobID: w1wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 3, TemplateVersionID: t1v2.ID, JobID: w1wb3pj.ID, CreatedAt: now.Add(-4 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w2wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 4, TemplateVersionID: t2v1.ID, JobID: w2wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 4, TemplateVersionID: t2v1.ID, JobID: w2wb1pj.ID, CreatedAt: now.Add(-5 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w2wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-4 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 5, TemplateVersionID: t2v2.ID, JobID: w2wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 5, TemplateVersionID: t2v2.ID, JobID: w2wb2pj.ID, CreatedAt: now.Add(-4 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w2wb3pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-3 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 6, TemplateVersionID: t2v2.ID, JobID: w2wb3pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w2.ID, BuildNumber: 6, TemplateVersionID: t2v2.ID, JobID: w2wb3pj.ID, CreatedAt: now.Add(-3 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w3wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-3 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w3.ID, BuildNumber: 7, TemplateVersionID: t1v1.ID, JobID: w3wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w3.ID, BuildNumber: 7, TemplateVersionID: t1v1.ID, JobID: w3wb1pj.ID, CreatedAt: now.Add(-3 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w4wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 8, TemplateVersionID: t2v1.ID, JobID: w4wb1pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) - w4wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-1 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 9, TemplateVersionID: t2v2.ID, JobID: w4wb2pj.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 8, TemplateVersionID: t2v1.ID, JobID: w4wb1pj.ID, CreatedAt: now.Add(-6 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w4wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 9, TemplateVersionID: t2v2.ID, JobID: w4wb2pj.ID, CreatedAt: now.Add(-dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) // Database is ready, so we can clear notifications queue notifEnq.Clear() @@ -165,8 +165,14 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Equal(t, notifEnq.Sent[3].Data["report_frequency"], "week") // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") - // Given: 6 days later (less than report frequency) + // Given: 6 days later (less than report frequency), and failed build clk.Advance(6 * dayDuration).MustWait(context.Background()) + + now = clk.Now() + + w1wb4pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 4, TemplateVersionID: t1v2.ID, JobID: w1wb4pj.ID, CreatedAt: now.Add(-dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + notifEnq.Clear() // When @@ -175,6 +181,34 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Then require.Empty(t, notifEnq.Sent) // no notifications as it is too early. + + // Given: 1 day 1 hour later + clk.Advance(dayDuration + time.Hour).MustWait(context.Background()) + notifEnq.Clear() + + // When + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + require.NoError(t, err) + + // Then + require.Len(t, notifEnq.Sent, 2) // this time a failed job should be reported + require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin1.ID) + require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[0].Labels["template_name"], t1.Name) + require.Equal(t, notifEnq.Sent[0].Labels["template_display_name"], t1.DisplayName) + require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(1)) + require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(1)) + require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") + + require.Equal(t, notifEnq.Sent[1].UserID, templateAdmin2.ID) + require.Equal(t, notifEnq.Sent[1].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[1].Labels["template_name"], t1.Name) + require.Equal(t, notifEnq.Sent[1].Labels["template_display_name"], t1.DisplayName) + require.Equal(t, notifEnq.Sent[1].Data["failed_builds"], int64(1)) + require.Equal(t, notifEnq.Sent[1].Data["total_builds"], int64(1)) + require.Equal(t, notifEnq.Sent[1].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[1].Data["template_versions"], "?") }) t.Run("NoFailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { @@ -187,11 +221,6 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // TODO }) - t.Run("StaleFailedBuilds_TemplateAdminOptIn_NoReport_Cleanup", func(t *testing.T) { - t.Parallel() - // TODO - }) - t.Run("FreshTemplate_FailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { t.Parallel() // TODO From 0cceede845ebe42265f690ae0643247d80431789 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 11:45:12 +0200 Subject: [PATCH 071/122] more tests --- .../reports/generator_internal_test.go | 108 +++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index e956c18b44877..77c54100893ba 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -213,12 +213,116 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Run("NoFailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { t.Parallel() - // TODO + + // Setup + ctx, logger, db, ps, notifEnq, clk := setup(t) + + // Given + // Organization + org := dbgen.Organization(t, db, database.Organization{}) + + // Template admins + templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID}) + + // Regular users + user1 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) + + // Templates + t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) + + // Template versions + t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + + // Workspaces + w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) + + now := clk.Now() + + // Workspace builds + w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-6 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v1.ID, JobID: w1wb2pj.ID, CreatedAt: now.Add(-5 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + // Database is ready, so we can clear notifications queue + notifEnq.Clear() + + // When + err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + + // Then + require.NoError(t, err) + + require.Len(t, notifEnq.Sent, 0) // all jobs succeeded so no report }) t.Run("FailedBuilds_TemplateAdminOptOut_NoReport", func(t *testing.T) { t.Parallel() - // TODO + + if !dbtestutil.WillUsePostgres() { + t.Skip("notification preferences depend on database trigger") + } + + // Setup + ctx, logger, db, ps, notifEnq, clk := setup(t) + + // Given + // Organization + org := dbgen.Organization(t, db, database.Organization{}) + + // Template admins + templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID}) + templateAdmin2 := dbgen.User(t, db, database.User{Username: "template-admin-2", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin2.ID, OrganizationID: org.ID}) + _, err := db.UpdateUserNotificationPreferences(ctx, database.UpdateUserNotificationPreferencesParams{ + UserID: templateAdmin2.ID, + NotificationTemplateIds: []uuid.UUID{notifications.TemplateWorkspaceBuildsFailedReport}, + Disableds: []bool{true}, + }) + require.NoError(t, err) + + // Regular users + user1 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) + + // Templates + t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) + + // Template versions + t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + + // Workspaces + w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) + + now := clk.Now() + + // Workspace builds + w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-6 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v1.ID, JobID: w1wb2pj.ID, CreatedAt: now.Add(-5 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + // Database is ready, so we can clear notifications queue + notifEnq.Clear() + + // When + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + + // Then + require.NoError(t, err) + + require.Len(t, notifEnq.Sent, 1) // one job failed, but only one template admin enabled reports + require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin1.ID) + require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) + require.Equal(t, notifEnq.Sent[0].Labels["template_name"], t1.Name) + require.Equal(t, notifEnq.Sent[0].Labels["template_display_name"], t1.DisplayName) + require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(1)) + require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(2)) + require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") + // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") }) t.Run("FreshTemplate_FailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { From 1f06b8672a71d5aba9258c6b3b8a958591a0e950 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 12:06:54 +0200 Subject: [PATCH 072/122] WIP --- coderd/notifications/reports/generator.go | 3 +- .../reports/generator_internal_test.go | 94 ++++--------------- 2 files changed, 20 insertions(+), 77 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 9e6e9ff562b00..fa15f9a70d4db 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -104,7 +104,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat now := clk.Now() since := now.Add(-failedWorkspaceBuildsReportFrequencyDays * 24 * time.Hour) - // TODO skip new templates statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(since).UTC()) if err != nil { return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) @@ -249,6 +248,8 @@ func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.G "build_number": failedBuild.WorkspaceBuildNumber, }) templateVersions[c-1]["failed_builds"] = builds + //nolint:errorlint,forcetypeassert // only this function prepares the notification model + templateVersions[c-1]["failed_count"] = templateVersions[c-1]["failed_count"].(int) + 1 } return map[string]any{ diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 77c54100893ba..a49f466ca1e20 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -75,8 +75,6 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) user2 := dbgen.User(t, db, database.User{}) _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user2.ID, OrganizationID: org.ID}) - user3 := dbgen.User(t, db, database.User{}) - // user in some other org // Templates t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) @@ -91,7 +89,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Workspaces w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) w2 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t2.ID, OwnerID: user2.ID, OrganizationID: org.ID}) - w3 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user3.ID, OrganizationID: org.ID}) + w3 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) w4 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t2.ID, OwnerID: user2.ID, OrganizationID: org.ID}) now := clk.Now() @@ -136,7 +134,23 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(3)) require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(4)) require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") + require.Equal(t, notifEnq.Sent[0].Data["template_versions"], []map[string]interface{}{ + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(7), "workspace_name": w3.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(1), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 2, + "template_version_name": t1v1.Name, + }, + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(3), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 1, + "template_version_name": t1v2.Name, + }, + }) require.Equal(t, notifEnq.Sent[1].UserID, templateAdmin2.ID) require.Equal(t, notifEnq.Sent[1].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) @@ -257,78 +271,6 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Len(t, notifEnq.Sent, 0) // all jobs succeeded so no report }) - - t.Run("FailedBuilds_TemplateAdminOptOut_NoReport", func(t *testing.T) { - t.Parallel() - - if !dbtestutil.WillUsePostgres() { - t.Skip("notification preferences depend on database trigger") - } - - // Setup - ctx, logger, db, ps, notifEnq, clk := setup(t) - - // Given - // Organization - org := dbgen.Organization(t, db, database.Organization{}) - - // Template admins - templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) - _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID}) - templateAdmin2 := dbgen.User(t, db, database.User{Username: "template-admin-2", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) - _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin2.ID, OrganizationID: org.ID}) - _, err := db.UpdateUserNotificationPreferences(ctx, database.UpdateUserNotificationPreferencesParams{ - UserID: templateAdmin2.ID, - NotificationTemplateIds: []uuid.UUID{notifications.TemplateWorkspaceBuildsFailedReport}, - Disableds: []bool{true}, - }) - require.NoError(t, err) - - // Regular users - user1 := dbgen.User(t, db, database.User{}) - _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) - - // Templates - t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) - - // Template versions - t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) - - // Workspaces - w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) - - now := clk.Now() - - // Workspace builds - w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-6 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) - w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v1.ID, JobID: w1wb2pj.ID, CreatedAt: now.Add(-5 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) - - // Database is ready, so we can clear notifications queue - notifEnq.Clear() - - // When - err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) - - // Then - require.NoError(t, err) - - require.Len(t, notifEnq.Sent, 1) // one job failed, but only one template admin enabled reports - require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin1.ID) - require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[0].Labels["template_name"], t1.Name) - require.Equal(t, notifEnq.Sent[0].Labels["template_display_name"], t1.DisplayName) - require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(1)) - require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(2)) - require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") - }) - - t.Run("FreshTemplate_FailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { - t.Parallel() - // TODO - }) } func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.Pubsub, *testutil.FakeNotificationsEnqueuer, *quartz.Mock) { From a1c89ec8b5047478c2e691cac1fc9617675a53d0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 12:38:27 +0200 Subject: [PATCH 073/122] tests done --- .../reports/generator_internal_test.go | 161 +++++++++--------- 1 file changed, 83 insertions(+), 78 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index a49f466ca1e20..4475bc0b7ca5f 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -52,7 +52,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Empty(t, notifEnq.Sent) }) - t.Run("FailedBuilds_TemplateAdminOptIn_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { + t.Run("FailedBuilds_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { t.Parallel() // Setup @@ -68,7 +68,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { templateAdmin2 := dbgen.User(t, db, database.User{Username: "template-admin-2", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin2.ID, OrganizationID: org.ID}) _ = dbgen.User(t, db, database.User{Name: "template-admin-3", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) - // template admin in some other org + // template admin in some other org, they should not receive any notification // Regular users user1 := dbgen.User(t, db, database.User{}) @@ -84,7 +84,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) t1v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) t2v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-2-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) - t2v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) + t2v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-2-version-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, JobID: uuid.New()}) // Workspaces w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) @@ -121,63 +121,65 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { notifEnq.Clear() // When - err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) // Then require.NoError(t, err) require.Len(t, notifEnq.Sent, 4) // 2 templates, 2 template admins - require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin1.ID) - require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[0].Labels["template_name"], t1.Name) - require.Equal(t, notifEnq.Sent[0].Labels["template_display_name"], t1.DisplayName) - require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(3)) - require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(4)) - require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") - require.Equal(t, notifEnq.Sent[0].Data["template_versions"], []map[string]interface{}{ - { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(7), "workspace_name": w3.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(1), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { + require.Equal(t, templateAdmin.ID, notifEnq.Sent[i].UserID) + require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notifEnq.Sent[i].TemplateID) + require.Equal(t, t1.Name, notifEnq.Sent[i].Labels["template_name"]) + require.Equal(t, t1.DisplayName, notifEnq.Sent[i].Labels["template_display_name"]) + require.Equal(t, int64(3), notifEnq.Sent[i].Data["failed_builds"]) + require.Equal(t, int64(4), notifEnq.Sent[i].Data["total_builds"]) + require.Equal(t, "week", notifEnq.Sent[i].Data["report_frequency"]) + require.Equal(t, []map[string]interface{}{ + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(7), "workspace_name": w3.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(1), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 2, + "template_version_name": t1v1.Name, }, - "failed_count": 2, - "template_version_name": t1v1.Name, - }, - { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(3), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(3), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 1, + "template_version_name": t1v2.Name, }, - "failed_count": 1, - "template_version_name": t1v2.Name, - }, - }) - - require.Equal(t, notifEnq.Sent[1].UserID, templateAdmin2.ID) - require.Equal(t, notifEnq.Sent[1].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[1].Labels["template_name"], t1.Name) - require.Equal(t, notifEnq.Sent[1].Labels["template_display_name"], t1.DisplayName) - require.Equal(t, notifEnq.Sent[1].Data["failed_builds"], int64(3)) - require.Equal(t, notifEnq.Sent[1].Data["total_builds"], int64(4)) - require.Equal(t, notifEnq.Sent[1].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[1].Data["template_versions"], "?") - - require.Equal(t, notifEnq.Sent[2].UserID, templateAdmin1.ID) - require.Equal(t, notifEnq.Sent[2].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[2].Labels["template_name"], t2.Name) - require.Equal(t, notifEnq.Sent[2].Labels["template_display_name"], t2.DisplayName) - require.Equal(t, notifEnq.Sent[2].Data["failed_builds"], int64(3)) - require.Equal(t, notifEnq.Sent[2].Data["total_builds"], int64(5)) - require.Equal(t, notifEnq.Sent[2].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") - - require.Equal(t, notifEnq.Sent[3].UserID, templateAdmin2.ID) - require.Equal(t, notifEnq.Sent[3].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[3].Labels["template_name"], t2.Name) - require.Equal(t, notifEnq.Sent[3].Labels["template_display_name"], t2.DisplayName) - require.Equal(t, notifEnq.Sent[3].Data["failed_builds"], int64(3)) - require.Equal(t, notifEnq.Sent[3].Data["total_builds"], int64(5)) - require.Equal(t, notifEnq.Sent[3].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") + }, notifEnq.Sent[i].Data["template_versions"]) + } + + for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { + require.Equal(t, templateAdmin.ID, notifEnq.Sent[i+2].UserID) + require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notifEnq.Sent[i+2].TemplateID) + require.Equal(t, t2.Name, notifEnq.Sent[i+2].Labels["template_name"]) + require.Equal(t, t2.DisplayName, notifEnq.Sent[i+2].Labels["template_display_name"]) + require.Equal(t, int64(3), notifEnq.Sent[i+2].Data["failed_builds"]) + require.Equal(t, int64(5), notifEnq.Sent[i+2].Data["total_builds"]) + require.Equal(t, "week", notifEnq.Sent[i+2].Data["report_frequency"]) + require.Equal(t, []map[string]interface{}{ + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(8), "workspace_name": w4.Name, "workspace_owner_username": user2.Username}, + }, + "failed_count": 1, + "template_version_name": t2v1.Name, + }, + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(6), "workspace_name": w2.Name, "workspace_owner_username": user2.Username}, + {"build_number": int32(5), "workspace_name": w2.Name, "workspace_owner_username": user2.Username}, + }, + "failed_count": 2, + "template_version_name": t2v2.Name, + }, + }, notifEnq.Sent[i+2].Data["template_versions"]) + } // Given: 6 days later (less than report frequency), and failed build clk.Advance(6 * dayDuration).MustWait(context.Background()) @@ -185,12 +187,12 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { now = clk.Now() w1wb4pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 4, TemplateVersionID: t1v2.ID, JobID: w1wb4pj.ID, CreatedAt: now.Add(-dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 77, TemplateVersionID: t1v2.ID, JobID: w1wb4pj.ID, CreatedAt: now.Add(-dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) notifEnq.Clear() // When - err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) require.NoError(t, err) // Then @@ -201,31 +203,32 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { notifEnq.Clear() // When - err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) require.NoError(t, err) // Then - require.Len(t, notifEnq.Sent, 2) // this time a failed job should be reported - require.Equal(t, notifEnq.Sent[0].UserID, templateAdmin1.ID) - require.Equal(t, notifEnq.Sent[0].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[0].Labels["template_name"], t1.Name) - require.Equal(t, notifEnq.Sent[0].Labels["template_display_name"], t1.DisplayName) - require.Equal(t, notifEnq.Sent[0].Data["failed_builds"], int64(1)) - require.Equal(t, notifEnq.Sent[0].Data["total_builds"], int64(1)) - require.Equal(t, notifEnq.Sent[0].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[0].Data["template_versions"], "?") - - require.Equal(t, notifEnq.Sent[1].UserID, templateAdmin2.ID) - require.Equal(t, notifEnq.Sent[1].TemplateID, notifications.TemplateWorkspaceBuildsFailedReport) - require.Equal(t, notifEnq.Sent[1].Labels["template_name"], t1.Name) - require.Equal(t, notifEnq.Sent[1].Labels["template_display_name"], t1.DisplayName) - require.Equal(t, notifEnq.Sent[1].Data["failed_builds"], int64(1)) - require.Equal(t, notifEnq.Sent[1].Data["total_builds"], int64(1)) - require.Equal(t, notifEnq.Sent[1].Data["report_frequency"], "week") - // require.Contains(t, notifEnq.Sent[1].Data["template_versions"], "?") + require.Len(t, notifEnq.Sent, 2) // a new failed job should be reported + for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { + require.Equal(t, templateAdmin.ID, notifEnq.Sent[i].UserID) + require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notifEnq.Sent[i].TemplateID) + require.Equal(t, t1.Name, notifEnq.Sent[i].Labels["template_name"]) + require.Equal(t, t1.DisplayName, notifEnq.Sent[i].Labels["template_display_name"]) + require.Equal(t, int64(1), notifEnq.Sent[i].Data["failed_builds"]) + require.Equal(t, int64(1), notifEnq.Sent[i].Data["total_builds"]) + require.Equal(t, "week", notifEnq.Sent[i].Data["report_frequency"]) + require.Equal(t, []map[string]interface{}{ + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(77), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 1, + "template_version_name": t1v2.Name, + }, + }, notifEnq.Sent[i].Data["template_versions"]) + } }) - t.Run("NoFailedBuilds_TemplateAdminIn_NoReport", func(t *testing.T) { + t.Run("NoFailedBuilds_NoReport", func(t *testing.T) { t.Parallel() // Setup @@ -264,12 +267,12 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { notifEnq.Clear() // When - err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(db, logger), notifEnq, clk) + err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) // Then require.NoError(t, err) - require.Len(t, notifEnq.Sent, 0) // all jobs succeeded so no report + require.Len(t, notifEnq.Sent, 0) // all jobs succeeded so nothing to report }) } @@ -285,6 +288,8 @@ func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.P return ctx, logger, db, ps, notifyEnq, clk } -func authedDB(db database.Store, logger slog.Logger) database.Store { +func authedDB(t *testing.T, db database.Store, logger slog.Logger) database.Store { + t.Helper() + return dbauthz.New(db, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) } From 34cc4c3a261455e87ee1a576d05020969137f6c9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 12:47:26 +0200 Subject: [PATCH 074/122] cleanup --- .../reports/generator_internal_test.go | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index 4475bc0b7ca5f..ba0239cf527e7 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -55,6 +55,19 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Run("FailedBuilds_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { t.Parallel() + verifyNotification := func(t *testing.T, recipient database.User, notif *testutil.Notification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) { + t.Helper() + + require.Equal(t, recipient.ID, notif.UserID) + require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID) + require.Equal(t, tmpl.Name, notif.Labels["template_name"]) + require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"]) + require.Equal(t, failedBuilds, notif.Data["failed_builds"]) + require.Equal(t, totalBuilds, notif.Data["total_builds"]) + require.Equal(t, "week", notif.Data["report_frequency"]) + require.Equal(t, templateVersions, notif.Data["template_versions"]) + } + // Setup ctx, logger, db, ps, notifEnq, clk := setup(t) @@ -128,14 +141,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Len(t, notifEnq.Sent, 4) // 2 templates, 2 template admins for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { - require.Equal(t, templateAdmin.ID, notifEnq.Sent[i].UserID) - require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notifEnq.Sent[i].TemplateID) - require.Equal(t, t1.Name, notifEnq.Sent[i].Labels["template_name"]) - require.Equal(t, t1.DisplayName, notifEnq.Sent[i].Labels["template_display_name"]) - require.Equal(t, int64(3), notifEnq.Sent[i].Data["failed_builds"]) - require.Equal(t, int64(4), notifEnq.Sent[i].Data["total_builds"]) - require.Equal(t, "week", notifEnq.Sent[i].Data["report_frequency"]) - require.Equal(t, []map[string]interface{}{ + verifyNotification(t, templateAdmin, notifEnq.Sent[i], t1, 3, 4, []map[string]interface{}{ { "failed_builds": []map[string]interface{}{ {"build_number": int32(7), "workspace_name": w3.Name, "workspace_owner_username": user1.Username}, @@ -151,18 +157,11 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { "failed_count": 1, "template_version_name": t1v2.Name, }, - }, notifEnq.Sent[i].Data["template_versions"]) + }) } for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { - require.Equal(t, templateAdmin.ID, notifEnq.Sent[i+2].UserID) - require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notifEnq.Sent[i+2].TemplateID) - require.Equal(t, t2.Name, notifEnq.Sent[i+2].Labels["template_name"]) - require.Equal(t, t2.DisplayName, notifEnq.Sent[i+2].Labels["template_display_name"]) - require.Equal(t, int64(3), notifEnq.Sent[i+2].Data["failed_builds"]) - require.Equal(t, int64(5), notifEnq.Sent[i+2].Data["total_builds"]) - require.Equal(t, "week", notifEnq.Sent[i+2].Data["report_frequency"]) - require.Equal(t, []map[string]interface{}{ + verifyNotification(t, templateAdmin, notifEnq.Sent[i+2], t2, 3, 5, []map[string]interface{}{ { "failed_builds": []map[string]interface{}{ {"build_number": int32(8), "workspace_name": w4.Name, "workspace_owner_username": user2.Username}, @@ -178,7 +177,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { "failed_count": 2, "template_version_name": t2v2.Name, }, - }, notifEnq.Sent[i+2].Data["template_versions"]) + }) } // Given: 6 days later (less than report frequency), and failed build @@ -209,14 +208,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Then require.Len(t, notifEnq.Sent, 2) // a new failed job should be reported for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { - require.Equal(t, templateAdmin.ID, notifEnq.Sent[i].UserID) - require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notifEnq.Sent[i].TemplateID) - require.Equal(t, t1.Name, notifEnq.Sent[i].Labels["template_name"]) - require.Equal(t, t1.DisplayName, notifEnq.Sent[i].Labels["template_display_name"]) - require.Equal(t, int64(1), notifEnq.Sent[i].Data["failed_builds"]) - require.Equal(t, int64(1), notifEnq.Sent[i].Data["total_builds"]) - require.Equal(t, "week", notifEnq.Sent[i].Data["report_frequency"]) - require.Equal(t, []map[string]interface{}{ + verifyNotification(t, templateAdmin, notifEnq.Sent[i], t1, 1, 1, []map[string]interface{}{ { "failed_builds": []map[string]interface{}{ {"build_number": int32(77), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, @@ -224,7 +216,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { "failed_count": 1, "template_version_name": t1v2.Name, }, - }, notifEnq.Sent[i].Data["template_versions"]) + }) } }) @@ -290,6 +282,5 @@ func setup(t *testing.T) (context.Context, slog.Logger, database.Store, pubsub.P func authedDB(t *testing.T, db database.Store, logger slog.Logger) database.Store { t.Helper() - return dbauthz.New(db, rbac.NewAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) } From 5911ca945b22f78963f2d72cb88920bd2cdec512 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 13 Sep 2024 14:54:13 +0200 Subject: [PATCH 075/122] fix: LockIDNotificationsReportGenerator --- coderd/database/lock.go | 2 +- coderd/notifications/reports/generator.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/lock.go b/coderd/database/lock.go index 3e54577db59db..0ebf6b0f1428e 100644 --- a/coderd/database/lock.go +++ b/coderd/database/lock.go @@ -10,7 +10,7 @@ const ( LockIDEnterpriseDeploymentSetup LockIDDBRollup LockIDDBPurge - LockIDReportGenerator + LockIDNotificationsReportGenerator ) // GenLockID generates a unique and consistent lock ID from a given string. diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index fa15f9a70d4db..b6095e97349d5 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -42,7 +42,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto // Start a transaction to grab advisory lock, we don't want to run generator jobs at the same time (multiple replicas). if err := db.InTx(func(tx database.Store) error { // Acquire a lock to ensure that only one instance of the generator is running at a time. - ok, err := tx.TryAcquireLock(ctx, database.LockIDReportGenerator) + ok, err := tx.TryAcquireLock(ctx, database.LockIDNotificationsReportGenerator) if err != nil { return err } From dda9bf581f79b5a74b72f6a45b8876b2263f02b6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 12:26:19 +0200 Subject: [PATCH 076/122] Address Mathias' feedback --- coderd/database/queries/workspacebuilds.sql | 4 +-- coderd/notifications/reports/generator.go | 39 +++++++++------------ 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 39776567da80c..b30530181de1c 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -200,7 +200,7 @@ JOIN templates AS t ON w.template_id = t.id WHERE - wb.created_at > @since + wb.created_at >= @since AND pj.completed_at IS NOT NULL GROUP BY w.template_id, template_name, template_display_name, template_organization_id; @@ -235,6 +235,6 @@ ON wb.template_version_id = tv.id WHERE w.template_id = $1 - AND wb.created_at > @since + AND wb.created_at >= @since AND pj.completed_at IS NOT NULL AND pj.job_status = 'failed'; diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index b6095e97349d5..b4d12b6ffdc9e 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -3,7 +3,6 @@ package reports import ( "context" "database/sql" - "fmt" "io" "slices" "sort" @@ -37,6 +36,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto // Start the ticker with the initial delay. ticker := clk.NewTicker(delay) + ticker.Stop() doTick := func(start time.Time) { defer ticker.Reset(delay) // Start a transaction to grab advisory lock, we don't want to run generator jobs at the same time (multiple replicas). @@ -98,11 +98,14 @@ func (i *reportGenerator) Close() error { return nil } -const failedWorkspaceBuildsReportFrequencyDays = 7 +const ( + failedWorkspaceBuildsReportFrequency = 7 * 24 * time.Hour + failedWorkspaceBuildsReportFrequencyLabel = "week" +) func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { now := clk.Now() - since := now.Add(-failedWorkspaceBuildsReportFrequencyDays * 24 * time.Hour) + since := now.Add(-failedWorkspaceBuildsReportFrequency) statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(since).UTC()) if err != nil { @@ -128,7 +131,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } // There are some failed builds, so we have to prepare input data for the report. - reportData = buildDataForReportFailedWorkspaceBuilds(failedWorkspaceBuildsReportFrequencyDays, stats, failedBuilds) + reportData = buildDataForReportFailedWorkspaceBuilds(stats, failedBuilds) } templateAdmins, err := findTemplateAdmins(ctx, db, stats) @@ -147,7 +150,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat continue } - if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequencyDays*24*time.Hour).After(now) { + if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequency).After(now) { // report generated recently, no need to send it now continue } @@ -191,7 +194,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat err = db.DeleteOldReportGeneratorLogs(ctx, database.DeleteOldReportGeneratorLogsParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - Before: dbtime.Time(now.Add(-failedWorkspaceBuildsReportFrequencyDays*24*time.Hour - time.Hour)).UTC(), + Before: dbtime.Time(now.Add(-failedWorkspaceBuildsReportFrequency - time.Hour)).UTC(), }) if err != nil { return xerrors.Errorf("unable to delete old report generator logs: %w", err) @@ -199,19 +202,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return nil } -func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any { - // Format frequency label - var frequencyLabel string - if frequencyDays == 7 { - frequencyLabel = "week" - } else { - var plural string - if frequencyDays > 1 { - plural = "s" - } - frequencyLabel = fmt.Sprintf("%d day%s", frequencyDays, plural) - } - +func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any { // Sorting order: template_version_name ASC, workspace build number DESC sort.Slice(failedBuilds, func(i, j int) bool { if failedBuilds[i].TemplateVersionName != failedBuilds[j].TemplateVersionName { @@ -240,22 +231,24 @@ func buildDataForReportFailedWorkspaceBuilds(frequencyDays int, stats database.G continue } + tv := templateVersions[c-1] //nolint:errorlint,forcetypeassert // only this function prepares the notification model - builds := templateVersions[c-1]["failed_builds"].([]map[string]any) + builds := tv["failed_builds"].([]map[string]any) builds = append(builds, map[string]any{ "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, "workspace_name": failedBuild.WorkspaceName, "build_number": failedBuild.WorkspaceBuildNumber, }) - templateVersions[c-1]["failed_builds"] = builds + tv["failed_builds"] = builds //nolint:errorlint,forcetypeassert // only this function prepares the notification model - templateVersions[c-1]["failed_count"] = templateVersions[c-1]["failed_count"].(int) + 1 + tv["failed_count"] = tv["failed_count"].(int) + 1 + templateVersions[c-1] = tv } return map[string]any{ "failed_builds": stats.FailedBuilds, "total_builds": stats.TotalBuilds, - "report_frequency": frequencyLabel, + "report_frequency": failedWorkspaceBuildsReportFrequencyLabel, "template_versions": templateVersions, } } From 15f83c09655fcfc28b3294698a99f86261f86c36 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 10:37:44 +0000 Subject: [PATCH 077/122] makegen --- coderd/database/queries.sql.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 619b91d26b0f5..54208899427b2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12699,7 +12699,7 @@ ON wb.template_version_id = tv.id WHERE w.template_id = $1 - AND wb.created_at > $2 + AND wb.created_at >= $2 AND pj.completed_at IS NOT NULL AND pj.job_status = 'failed' ` @@ -13022,7 +13022,7 @@ JOIN templates AS t ON w.template_id = t.id WHERE - wb.created_at > $1 + wb.created_at >= $1 AND pj.completed_at IS NOT NULL GROUP BY w.template_id, template_name, template_display_name, template_organization_id From 63064076dca6e120916229651b89e5eaa7602298 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 12:40:14 +0200 Subject: [PATCH 078/122] fmt --- coderd/notifications/reports/generator.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index b4d12b6ffdc9e..c84e71f3504f1 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -8,13 +8,10 @@ import ( "sort" "time" - "golang.org/x/xerrors" - "cdr.dev/slog" - - "github.com/google/uuid" - "github.com/coder/quartz" + "github.com/google/uuid" + "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" From e25803527009da65c968c70c612ec6ba009636b8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 12:46:06 +0200 Subject: [PATCH 079/122] more --- coderd/database/migrations/000250_email_reports.up.sql | 7 +++---- coderd/notifications/reports/generator.go | 5 +++-- .../TemplateWorkspaceBuildsFailedReport-body.md.golden | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/database/migrations/000250_email_reports.up.sql b/coderd/database/migrations/000250_email_reports.up.sql index 3285f0680821b..f4c8b2f4df19f 100644 --- a/coderd/database/migrations/000250_email_reports.up.sql +++ b/coderd/database/migrations/000250_email_reports.up.sql @@ -2,7 +2,7 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "gr VALUES ('34a20db2-e9cc-4a93-b0e4-8569699d7a00', 'Report: Workspace Builds Failed For Template', E'Workspace builds failed for template "{{.Labels.template_display_name}}"', E'Hi {{.UserName}}, -Template **{{.Labels.template_display_name}}** has failed to build {{.Data.failed_builds}}/{{.Data.total_builds}} times over the last {{.Data.report_frequency}} and may be unstable. +Template **{{.Labels.template_display_name}}** has failed to build {{.Data.failed_builds}}/{{.Data.total_builds}} times over the last {{.Data.report_frequency}}. **Report:** {{range $version := .Data.template_versions}} @@ -25,8 +25,7 @@ CREATE TABLE report_generator_logs notification_template_id uuid NOT NULL, last_generated_at timestamp with time zone NOT NULL, - PRIMARY KEY (user_id, notification_template_id), - UNIQUE (user_id, notification_template_id) + PRIMARY KEY (user_id, notification_template_id) ); -COMMENT ON TABLE report_generator_logs IS 'Logs with generated reports for users.'; +COMMENT ON TABLE report_generator_logs IS 'Log of generated reports for users.'; diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index c84e71f3504f1..82ccdb0ecb2fe 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -8,11 +8,12 @@ import ( "sort" "time" - "cdr.dev/slog" - "github.com/coder/quartz" "github.com/google/uuid" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/coder/quartz" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-body.md.golden index cbd9b2e690c60..e896a0a8c9e51 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceBuildsFailedReport-body.md.golden @@ -1,6 +1,6 @@ Hi Bobby, -Template **Bobby First Template** has failed to build 4/55 times over the last week and may be unstable. +Template **Bobby First Template** has failed to build 4/55 times over the last week. **Report:** From 48da1cd39ac38f892a7f8d7170c42c8e167803e0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 12:57:09 +0200 Subject: [PATCH 080/122] reports --- coderd/notifications/reports/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 82ccdb0ecb2fe..8dc2f621d319d 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -51,7 +51,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueuer, clk) if err != nil { - logger.Debug(ctx, "unable to report failed workspace builds") + logger.Debug(ctx, "unable to generate reports with failed workspace builds") return err } From 309c6004b64484714af786018e7dbefeca359826 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:08:29 +0200 Subject: [PATCH 081/122] payload --- coderd/notifications/types/payload.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/types/payload.go b/coderd/notifications/types/payload.go index fa03a1acba639..dbd21c29be517 100644 --- a/coderd/notifications/types/payload.go +++ b/coderd/notifications/types/payload.go @@ -17,5 +17,5 @@ type MessagePayload struct { Actions []TemplateAction `json:"actions"` Labels map[string]string `json:"labels"` - Data map[string]any `json:"data,omitempty"` + Data map[string]any `json:"data"` } From 8211ee66961decbc0ad46825c9910faa99fc3c17 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:09:21 +0200 Subject: [PATCH 082/122] nil-omitted --- coderd/notifications/enqueuer.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/notifications/enqueuer.go b/coderd/notifications/enqueuer.go index 82c05736d1aac..fc3863444e986 100644 --- a/coderd/notifications/enqueuer.go +++ b/coderd/notifications/enqueuer.go @@ -54,7 +54,6 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem // Enqueue queues a notification message for later delivery, assumes no structured input data. func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { - // "nil" data will be omitted while building the JSON payload. return s.EnqueueData(ctx, userID, templateID, labels, nil, createdBy, targets...) } From a8aea9faa86eb3292524d0d2a8803a89e883c16d Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:09:51 +0200 Subject: [PATCH 083/122] failed-lock --- coderd/notifications/reports/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 8dc2f621d319d..614e7c8a8111e 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -42,7 +42,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto // Acquire a lock to ensure that only one instance of the generator is running at a time. ok, err := tx.TryAcquireLock(ctx, database.LockIDNotificationsReportGenerator) if err != nil { - return err + return xerrors.Errorf("failed to acquire report generator lock: %w", err) } if !ok { logger.Debug(ctx, "unable to acquire lock for generating periodic reports, skipping") From da6b0a38b7122b7f28e1fed2e78415e121874a47 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:10:51 +0200 Subject: [PATCH 084/122] xerrors --- coderd/notifications/reports/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 614e7c8a8111e..bd890dde9c53b 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -52,7 +52,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueuer, clk) if err != nil { logger.Debug(ctx, "unable to generate reports with failed workspace builds") - return err + return xerrors.Errorf("unable to generate reports with failed workspace builds: %w", err) } logger.Info(ctx, "report generator finished", slog.F("duration", clk.Since(start))) From 4afc23bb7e723e4984dfd8c28d3cb36803d096bd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:12:29 +0200 Subject: [PATCH 085/122] c=0 --- coderd/notifications/reports/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index bd890dde9c53b..99e00567f31d0 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -214,7 +214,7 @@ func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildSta for _, failedBuild := range failedBuilds { c := len(templateVersions) - if len(templateVersions) == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName { + if c == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName { templateVersions = append(templateVersions, map[string]any{ "template_version_name": failedBuild.TemplateVersionName, "failed_count": 1, From bf7737daf98ccaf6c9e394e9bea88d26727bc1de Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:14:34 +0200 Subject: [PATCH 086/122] log-debug --- coderd/notifications/reports/generator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 99e00567f31d0..f94adeb98257f 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -72,6 +72,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto for { select { case <-ctx.Done(): + logger.Debug(ctx, "closing report generator") return case tick := <-ticker.C: ticker.Stop() From 102a2456c894fbcb0e02b8fdfa578b7c021d04d0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:16:11 +0200 Subject: [PATCH 087/122] enqueue with data --- coderd/notifications/enqueuer.go | 6 +++--- coderd/notifications/reports/generator.go | 2 +- coderd/notifications/spec.go | 2 +- testutil/notifications.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/notifications/enqueuer.go b/coderd/notifications/enqueuer.go index fc3863444e986..260fcd2675278 100644 --- a/coderd/notifications/enqueuer.go +++ b/coderd/notifications/enqueuer.go @@ -54,12 +54,12 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem // Enqueue queues a notification message for later delivery, assumes no structured input data. func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { - return s.EnqueueData(ctx, userID, templateID, labels, nil, createdBy, targets...) + return s.EnqueueWithData(ctx, userID, templateID, labels, nil, createdBy, targets...) } // Enqueue queues a notification message for later delivery. // Messages will be dequeued by a notifier later and dispatched. -func (s *StoreEnqueuer) EnqueueData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { +func (s *StoreEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { metadata, err := s.store.FetchNewMessageMetadata(ctx, database.FetchNewMessageMetadataParams{ UserID: userID, NotificationTemplateID: templateID, @@ -170,7 +170,7 @@ func (*NoopEnqueuer) Enqueue(context.Context, uuid.UUID, uuid.UUID, map[string]s return nil, nil } -func (*NoopEnqueuer) EnqueueData(context.Context, uuid.UUID, uuid.UUID, map[string]string, map[string]any, string, ...uuid.UUID) (*uuid.UUID, error) { +func (*NoopEnqueuer) EnqueueWithData(context.Context, uuid.UUID, uuid.UUID, map[string]string, map[string]any, string, ...uuid.UUID) (*uuid.UUID, error) { // nolint:nilnil // irrelevant. return nil, nil } diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index f94adeb98257f..34fe4f7e323f6 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -166,7 +166,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat templateDisplayName = stats.TemplateName } - if _, err := enqueuer.EnqueueData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, + if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, map[string]string{ "template_name": stats.TemplateName, "template_display_name": templateDisplayName, diff --git a/coderd/notifications/spec.go b/coderd/notifications/spec.go index a078bfdc21242..b8ae063cc919e 100644 --- a/coderd/notifications/spec.go +++ b/coderd/notifications/spec.go @@ -33,5 +33,5 @@ type Handler interface { // Enqueuer enqueues a new notification message in the store and returns its ID, should it enqueue without failure. type Enqueuer interface { Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) - EnqueueData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) + EnqueueWithData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) } diff --git a/testutil/notifications.go b/testutil/notifications.go index c9b4bd63c980a..379218cd379e8 100644 --- a/testutil/notifications.go +++ b/testutil/notifications.go @@ -21,10 +21,10 @@ type Notification struct { } func (f *FakeNotificationsEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { - return f.EnqueueData(ctx, userID, templateID, labels, nil, createdBy, targets...) + return f.EnqueueWithData(ctx, userID, templateID, labels, nil, createdBy, targets...) } -func (f *FakeNotificationsEnqueuer) EnqueueData(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { +func (f *FakeNotificationsEnqueuer) EnqueueWithData(_ context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) { f.mu.Lock() defer f.mu.Unlock() From 2c9fc46afe987d04a0492215ee4791aef2d36b20 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:17:44 +0200 Subject: [PATCH 088/122] remove logger --- coderd/notifications/reports/generator.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 34fe4f7e323f6..f41ba8cde3921 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -145,7 +145,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { // sql.ErrNoRows: report not generated yet - logger.Error(ctx, "unable to get recent report generator log for user", slog.F("user_id", templateAdmin.ID), slog.Error(err)) continue } From cb7ca0ef43854c0eb8782b5dcb3f77ed1b384984 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 11:38:29 +0000 Subject: [PATCH 089/122] makegen --- coderd/database/dump.sql | 2 +- coderd/database/models.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index e7f1872e50bc1..2fb25faadc051 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -952,7 +952,7 @@ CREATE TABLE report_generator_logs ( last_generated_at timestamp with time zone NOT NULL ); -COMMENT ON TABLE report_generator_logs IS 'Logs with generated reports for users.'; +COMMENT ON TABLE report_generator_logs IS 'Log of generated reports for users.'; CREATE TABLE site_configs ( key character varying(256) NOT NULL, diff --git a/coderd/database/models.go b/coderd/database/models.go index e8124b0068b3c..96cc3e18e41cb 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2397,7 +2397,7 @@ type Replica struct { Primary bool `db:"primary" json:"primary"` } -// Logs with generated reports for users. +// Log of generated reports for users. type ReportGeneratorLog struct { UserID uuid.UUID `db:"user_id" json:"user_id"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` From 6582a484ad96d1dfe08cc0d61f924a78b0411cb0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:54:08 +0200 Subject: [PATCH 090/122] sort --- coderd/database/dbmem/dbmem.go | 4 ++++ coderd/database/queries/workspacebuilds.sql | 2 ++ coderd/notifications/reports/generator.go | 3 --- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8a9ed439d64fe..6f29c251dcf36 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5963,6 +5963,10 @@ func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, sin for _, ts := range templateStats { rows = append(rows, ts) } + + sort.Slice(rows, func(i, j int) bool { + return rows[i].TemplateName < rows[j].TemplateName + }) return rows, nil } diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index b30530181de1c..8aac7e76d3b38 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -204,6 +204,8 @@ WHERE AND pj.completed_at IS NOT NULL GROUP BY w.template_id, template_name, template_display_name, template_organization_id; +ORDER BY + template_name ASC; -- name: GetFailedWorkspaceBuildsByTemplateID :many SELECT diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index f41ba8cde3921..837a466fc426b 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -110,9 +110,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat if err != nil { return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) } - sort.Slice(statsRows, func(i, j int) bool { - return statsRows[i].TemplateName < statsRows[j].TemplateName - }) reportGeneratedNow := map[uuid.UUID]bool{} for _, stats := range statsRows { From c93351198890b786f3d2a03d83e8f6fa7b8b8e51 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 12:01:22 +0000 Subject: [PATCH 091/122] makegen --- coderd/database/queries.sql.go | 2 ++ coderd/database/queries/workspacebuilds.sql | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 54208899427b2..6fa478580a9e9 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -13026,6 +13026,8 @@ WHERE AND pj.completed_at IS NOT NULL GROUP BY w.template_id, template_name, template_display_name, template_organization_id +ORDER BY + template_name ASC ` type GetWorkspaceBuildStatsByTemplatesRow struct { diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 8aac7e76d3b38..5523adef8cf1c 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -203,7 +203,7 @@ WHERE wb.created_at >= @since AND pj.completed_at IS NOT NULL GROUP BY - w.template_id, template_name, template_display_name, template_organization_id; + w.template_id, template_name, template_display_name, template_organization_id ORDER BY template_name ASC; From 0cdf3eaf5df540c4c6bac3194f8bb1cf7c2c81f5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 14:21:08 +0200 Subject: [PATCH 092/122] GetFailedWorkspaceBuildsByTemplateID --- coderd/database/dbmem/dbmem.go | 6 ++++++ coderd/database/queries/workspacebuilds.sql | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6f29c251dcf36..92979aa614778 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -2592,6 +2592,12 @@ func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, }) } + sort.Slice(workspaceBuildStats, func(i, j int) bool { + if workspaceBuildStats[i].TemplateVersionName != workspaceBuildStats[j].TemplateVersionName { + return workspaceBuildStats[i].TemplateVersionName < workspaceBuildStats[j].TemplateVersionName + } + return workspaceBuildStats[i].WorkspaceBuildNumber > workspaceBuildStats[j].WorkspaceBuildNumber + }) return workspaceBuildStats, nil } diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 5523adef8cf1c..7050b61644e86 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -239,4 +239,6 @@ WHERE w.template_id = $1 AND wb.created_at >= @since AND pj.completed_at IS NOT NULL - AND pj.job_status = 'failed'; + AND pj.job_status = 'failed' +ORDER BY + tv.name ASC, wb.build_number DESC; From 6126e6b59d66b9e121986ea92c2cd03fffe60302 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 14:42:14 +0200 Subject: [PATCH 093/122] usersByIDs --- coderd/notifications/reports/generator.go | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 837a466fc426b..d3b3b7aca5e29 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -256,29 +256,31 @@ func findTemplateAdmins(ctx context.Context, db database.Store, stats database.G return nil, xerrors.Errorf("unable to fetch template admins: %w", err) } + var templateAdmins []database.GetUsersRow + usersByIDs := map[uuid.UUID]database.GetUsersRow{} + if len(usersByIDs) == 0 { + return templateAdmins, nil + } + var userIDs []uuid.UUID for _, user := range users { usersByIDs[user.ID] = user userIDs = append(userIDs, user.ID) } - var templateAdmins []database.GetUsersRow - if len(userIDs) > 0 { - orgIDsByMemberIDs, err := db.GetOrganizationIDsByMemberIDs(ctx, userIDs) - if err != nil { - return nil, xerrors.Errorf("unable to fetch organization IDs by member IDs: %w", err) - } + orgIDsByMemberIDs, err := db.GetOrganizationIDsByMemberIDs(ctx, userIDs) + if err != nil { + return nil, xerrors.Errorf("unable to fetch organization IDs by member IDs: %w", err) + } - for _, entry := range orgIDsByMemberIDs { - if slices.Contains(entry.OrganizationIDs, stats.TemplateOrganizationID) { - templateAdmins = append(templateAdmins, usersByIDs[entry.UserID]) - } + for _, entry := range orgIDsByMemberIDs { + if slices.Contains(entry.OrganizationIDs, stats.TemplateOrganizationID) { + templateAdmins = append(templateAdmins, usersByIDs[entry.UserID]) } } sort.Slice(templateAdmins, func(i, j int) bool { return templateAdmins[i].Username < templateAdmins[j].Username }) - return templateAdmins, nil } From 1000a50a354760bf49308dbf6f6488f11ce9a216 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 13:04:45 +0000 Subject: [PATCH 094/122] makegen --- coderd/database/queries.sql.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6fa478580a9e9..d1146e341673d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12702,6 +12702,8 @@ WHERE AND wb.created_at >= $2 AND pj.completed_at IS NOT NULL AND pj.job_status = 'failed' +ORDER BY + tv.name ASC, wb.build_number DESC ` type GetFailedWorkspaceBuildsByTemplateIDParams struct { From 314f080a7224cbd2731b85d107c690cb85c0cf73 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 16 Sep 2024 16:40:53 +0200 Subject: [PATCH 095/122] fix --- coderd/notifications/reports/generator.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index d3b3b7aca5e29..bcdf516dba29c 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -257,12 +257,11 @@ func findTemplateAdmins(ctx context.Context, db database.Store, stats database.G } var templateAdmins []database.GetUsersRow - - usersByIDs := map[uuid.UUID]database.GetUsersRow{} - if len(usersByIDs) == 0 { + if len(users) == 0 { return templateAdmins, nil } + usersByIDs := map[uuid.UUID]database.GetUsersRow{} var userIDs []uuid.UUID for _, user := range users { usersByIDs[user.ID] = user From 498b89b57c246e7d585f0af258576e8d33ea5559 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 09:46:36 +0200 Subject: [PATCH 096/122] processedUsers --- coderd/notifications/reports/generator.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index bcdf516dba29c..d060c196577d9 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -111,7 +111,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) } - reportGeneratedNow := map[uuid.UUID]bool{} + processedUsers := map[uuid.UUID]bool{} for _, stats := range statsRows { var failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow reportData := map[string]any{} @@ -150,7 +150,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat continue } - reportGeneratedNow[templateAdmin.ID] = true + processedUsers[templateAdmin.ID] = true if len(failedBuilds) == 0 { // no failed workspace builds, no need to send the report @@ -176,14 +176,14 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } - for recipient := range reportGeneratedNow { + for u := range processedUsers { err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ - UserID: recipient, + UserID: u, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, LastGeneratedAt: dbtime.Time(now).UTC(), }) if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("user_id", recipient), slog.Error(err)) + logger.Error(ctx, "unable to update report generator logs", slog.F("user_id", u), slog.Error(err)) } } From f115973e0d268fc22ae8b0183572a1de09b2c01b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 10:33:53 +0200 Subject: [PATCH 097/122] comment --- coderd/notifications/reports/generator.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index d060c196577d9..3115f5c6f2723 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -206,7 +206,11 @@ func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildSta return failedBuilds[i].WorkspaceBuildNumber > failedBuilds[j].WorkspaceBuildNumber }) - // Build notification model for template versions and failed workspace builds + // Build notification model for template versions and failed workspace builds. + // + // Failed builds are sorted by template version ascending, workspace build number descending. + // Review builds, group them by template versions, and assign to builds to template versions. + // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`. templateVersions := []map[string]any{} for _, failedBuild := range failedBuilds { c := len(templateVersions) From 01409caacf00de6c7b8a3753cf147ca93ca5e6e3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 10:37:54 +0200 Subject: [PATCH 098/122] WIP --- coderd/database/dbauthz/dbauthz.go | 12 +- coderd/database/dbauthz/dbauthz_test.go | 14 +- coderd/database/dbmem/dbmem.go | 6 +- coderd/database/dbmetrics/dbmetrics.go | 18 +- coderd/database/dbmock/dbmock.go | 36 +- coderd/database/migrations/migrate_test.go | 2 +- coderd/database/querier.go | 6 +- coderd/database/queries.sql.go | 40 +- coderd/database/queries/notifications.sql | 10 +- coderd/database/unique_constraint.go | 2 +- coderd/notifications/reports/generator.go | 6 +- site/src/api/typesGenerated.ts | 2377 -------------------- 12 files changed, 76 insertions(+), 2453 deletions(-) delete mode 100644 site/src/api/typesGenerated.ts diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 0f8195d7be3d4..08dc126f3b3a4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1144,11 +1144,11 @@ func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { return q.db.DeleteOldProvisionerDaemons(ctx) } -func (q *querier) DeleteOldReportGeneratorLogs(ctx context.Context, arg database.DeleteOldReportGeneratorLogsParams) error { +func (q *querier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg database.DeleteOldNotificationReportGeneratorLogsParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } - return q.db.DeleteOldReportGeneratorLogs(ctx, arg) + return q.db.DeleteOldNotificationReportGeneratorLogs(ctx, arg) } func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error { @@ -1877,11 +1877,11 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti return q.db.GetReplicasUpdatedAfter(ctx, updatedAt) } -func (q *querier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (q *querier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.ReportGeneratorLog{}, err } - return q.db.GetReportGeneratorLogByUserAndTemplate(ctx, arg) + return q.db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) } func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) { @@ -3964,11 +3964,11 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse return q.db.UpsertProvisionerDaemon(ctx, arg) } -func (q *querier) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { +func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err } - return q.db.UpsertReportGeneratorLog(ctx, arg) + return q.db.UpsertNotificationReportGeneratorLog(ctx, arg) } func (q *querier) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index e4dcaa0beea6c..0efef575a4427 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2735,8 +2735,8 @@ func (s *MethodTestSuite) TestSystemFunctions() { Value: "value", }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("DeleteOldReportGeneratorLogs", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.DeleteOldReportGeneratorLogsParams{ + s.Run("DeleteOldNotificationReportGeneratorLogs", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.DeleteOldNotificationReportGeneratorLogsParams{ Before: dbtime.Now(), NotificationTemplateID: uuid.New(), }).Asserts(rbac.ResourceSystem, policy.ActionDelete) @@ -2747,14 +2747,14 @@ func (s *MethodTestSuite) TestSystemFunctions() { Since: dbtime.Now(), }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetReportGeneratorLogByUserAndTemplate", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetNotificationReportGeneratorLogByUserAndTemplate", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) - _ = db.UpsertReportGeneratorLog(context.Background(), database.UpsertReportGeneratorLogParams{ + _ = db.UpsertNotificationReportGeneratorLog(context.Background(), database.UpsertNotificationReportGeneratorLogParams{ UserID: u.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, LastGeneratedAt: dbtime.Now(), }) - check.Args(database.GetReportGeneratorLogByUserAndTemplateParams{ + check.Args(database.GetNotificationReportGeneratorLogByUserAndTemplateParams{ UserID: u.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }).Asserts(rbac.ResourceSystem, policy.ActionRead) @@ -2762,8 +2762,8 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) { check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("UpsertReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpsertReportGeneratorLogParams{ + s.Run("UpsertNotificationReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.UpsertNotificationReportGeneratorLogParams{ UserID: uuid.New(), NotificationTemplateID: uuid.New(), LastGeneratedAt: dbtime.Now(), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 92979aa614778..e31203161607e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1709,7 +1709,7 @@ func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { return nil } -func (q *FakeQuerier) DeleteOldReportGeneratorLogs(_ context.Context, params database.DeleteOldReportGeneratorLogsParams) error { +func (q *FakeQuerier) DeleteOldNotificationReportGeneratorLogs(_ context.Context, params database.DeleteOldNotificationReportGeneratorLogsParams) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -3603,7 +3603,7 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time. return replicas, nil } -func (q *FakeQuerier) GetReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (q *FakeQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { err := validateDatabaseType(arg) if err != nil { return database.ReportGeneratorLog{}, err @@ -9429,7 +9429,7 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up return d, nil } -func (q *FakeQuerier) UpsertReportGeneratorLog(_ context.Context, arg database.UpsertReportGeneratorLogParams) error { +func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { err := validateDatabaseType(arg) if err != nil { return err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 5ff04b4a99f2d..f9a73c96c78b8 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -305,10 +305,10 @@ func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error { return r0 } -func (m metricsStore) DeleteOldReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldReportGeneratorLogsParams) error { +func (m metricsStore) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldNotificationReportGeneratorLogsParams) error { start := time.Now() - r0 := m.s.DeleteOldReportGeneratorLogs(ctx, frequencyDays) - m.queryLatencies.WithLabelValues("DeleteOldReportGeneratorLogs").Observe(time.Since(start).Seconds()) + r0 := m.s.DeleteOldNotificationReportGeneratorLogs(ctx, frequencyDays) + m.queryLatencies.WithLabelValues("DeleteOldNotificationReportGeneratorLogs").Observe(time.Since(start).Seconds()) return r0 } @@ -1012,10 +1012,10 @@ func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt tim return replicas, err } -func (m metricsStore) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (m metricsStore) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { start := time.Now() - r0, r1 := m.s.GetReportGeneratorLogByUserAndTemplate(ctx, arg) - m.queryLatencies.WithLabelValues("GetReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) + r0, r1 := m.s.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) + m.queryLatencies.WithLabelValues("GetNotificationReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) return r0, r1 } @@ -2510,10 +2510,10 @@ func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database. return r0, r1 } -func (m metricsStore) UpsertReportGeneratorLog(ctx context.Context, arg database.UpsertReportGeneratorLogParams) error { +func (m metricsStore) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { start := time.Now() - r0 := m.s.UpsertReportGeneratorLog(ctx, arg) - m.queryLatencies.WithLabelValues("UpsertReportGeneratorLog").Observe(time.Since(start).Seconds()) + r0 := m.s.UpsertNotificationReportGeneratorLog(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertNotificationReportGeneratorLog").Observe(time.Since(start).Seconds()) return r0 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index a0fc82222faa3..99f23d99433c2 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -500,18 +500,18 @@ func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldProvisionerDaemons", reflect.TypeOf((*MockStore)(nil).DeleteOldProvisionerDaemons), arg0) } -// DeleteOldReportGeneratorLogs mocks base method. -func (m *MockStore) DeleteOldReportGeneratorLogs(arg0 context.Context, arg1 database.DeleteOldReportGeneratorLogsParams) error { +// DeleteOldNotificationReportGeneratorLogs mocks base method. +func (m *MockStore) DeleteOldNotificationReportGeneratorLogs(arg0 context.Context, arg1 database.DeleteOldNotificationReportGeneratorLogsParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOldReportGeneratorLogs", arg0, arg1) + ret := m.ctrl.Call(m, "DeleteOldNotificationReportGeneratorLogs", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// DeleteOldReportGeneratorLogs indicates an expected call of DeleteOldReportGeneratorLogs. -func (mr *MockStoreMockRecorder) DeleteOldReportGeneratorLogs(arg0, arg1 any) *gomock.Call { +// DeleteOldNotificationReportGeneratorLogs indicates an expected call of DeleteOldNotificationReportGeneratorLogs. +func (mr *MockStoreMockRecorder) DeleteOldNotificationReportGeneratorLogs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldReportGeneratorLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldReportGeneratorLogs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNotificationReportGeneratorLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldNotificationReportGeneratorLogs), arg0, arg1) } // DeleteOldWorkspaceAgentLogs mocks base method. @@ -2062,19 +2062,19 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1) } -// GetReportGeneratorLogByUserAndTemplate mocks base method. -func (m *MockStore) GetReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +// GetNotificationReportGeneratorLogByUserAndTemplate mocks base method. +func (m *MockStore) GetNotificationReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetReportGeneratorLogByUserAndTemplate", arg0, arg1) + ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByUserAndTemplate", arg0, arg1) ret0, _ := ret[0].(database.ReportGeneratorLog) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetReportGeneratorLogByUserAndTemplate indicates an expected call of GetReportGeneratorLogByUserAndTemplate. -func (mr *MockStoreMockRecorder) GetReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { +// GetNotificationReportGeneratorLogByUserAndTemplate indicates an expected call of GetNotificationReportGeneratorLogByUserAndTemplate. +func (mr *MockStoreMockRecorder) GetNotificationReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetReportGeneratorLogByUserAndTemplate), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetNotificationReportGeneratorLogByUserAndTemplate), arg0, arg1) } // GetRuntimeConfig mocks base method. @@ -5269,18 +5269,18 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) } -// UpsertReportGeneratorLog mocks base method. -func (m *MockStore) UpsertReportGeneratorLog(arg0 context.Context, arg1 database.UpsertReportGeneratorLogParams) error { +// UpsertNotificationReportGeneratorLog mocks base method. +func (m *MockStore) UpsertNotificationReportGeneratorLog(arg0 context.Context, arg1 database.UpsertNotificationReportGeneratorLogParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertReportGeneratorLog", arg0, arg1) + ret := m.ctrl.Call(m, "UpsertNotificationReportGeneratorLog", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// UpsertReportGeneratorLog indicates an expected call of UpsertReportGeneratorLog. -func (mr *MockStoreMockRecorder) UpsertReportGeneratorLog(arg0, arg1 any) *gomock.Call { +// UpsertNotificationReportGeneratorLog indicates an expected call of UpsertNotificationReportGeneratorLog. +func (mr *MockStoreMockRecorder) UpsertNotificationReportGeneratorLog(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertReportGeneratorLog), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertNotificationReportGeneratorLog), arg0, arg1) } // UpsertRuntimeConfig mocks base method. diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index 6bf28fecfb6db..51e7fcc86cb03 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -268,7 +268,7 @@ func TestMigrateUpWithFixtures(t *testing.T) { "template_version_variables", "dbcrypt_keys", // having zero rows is a valid state for this table "template_version_workspace_tags", - "report_generator_logs", + "notification_report_generator_logs", } s := &tableStats{s: make(map[string]int)} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 366861239f30e..6a85e8546d5cf 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -88,7 +88,7 @@ type sqlcQuerier interface { // connectivity issues (no provisioner daemon activity since registration). DeleteOldProvisionerDaemons(ctx context.Context) error // Delete report generator logs that have been created at least a @before date. - DeleteOldReportGeneratorLogs(ctx context.Context, arg DeleteOldReportGeneratorLogsParams) error + DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error // If an agent hasn't connected in the last 7 days, we purge it's logs. // Exception: if the logs are related to the latest build, we keep those around. // Logs can take up a lot of space, so it's important we clean up frequently. @@ -204,7 +204,7 @@ type sqlcQuerier interface { GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) // Fetch the report generator log indicating recent activity. - GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) + GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) GetRuntimeConfig(ctx context.Context, key string) (string, error) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) @@ -491,7 +491,7 @@ type sqlcQuerier interface { UpsertOAuthSigningKey(ctx context.Context, value string) error UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) // Insert or update report generator logs with recent activity. - UpsertReportGeneratorLog(ctx context.Context, arg UpsertReportGeneratorLogParams) error + UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d1146e341673d..f0fb52e4cac0f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3128,7 +3128,7 @@ func (q *sqlQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, } const upsertJFrogXrayScanByWorkspaceAndAgentID = `-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec -INSERT INTO +INSERT INTO jfrog_xray_scans ( agent_id, workspace_id, @@ -3137,7 +3137,7 @@ INSERT INTO medium, results_url ) -VALUES +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_id) DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6 @@ -3552,18 +3552,18 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { return err } -const deleteOldReportGeneratorLogs = `-- name: DeleteOldReportGeneratorLogs :exec -DELETE FROM report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 +const DeleteOldNotificationReportGeneratorLogs = `-- name: DeleteOldNotificationReportGeneratorLogs :exec +DELETE FROM notification_report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 ` -type DeleteOldReportGeneratorLogsParams struct { +type DeleteOldNotificationReportGeneratorLogsParams struct { Before time.Time `db:"before" json:"before"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` } // Delete report generator logs that have been created at least a @before date. -func (q *sqlQuerier) DeleteOldReportGeneratorLogs(ctx context.Context, arg DeleteOldReportGeneratorLogsParams) error { - _, err := q.db.ExecContext(ctx, deleteOldReportGeneratorLogs, arg.Before, arg.NotificationTemplateID) +func (q *sqlQuerier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error { + _, err := q.db.ExecContext(ctx, DeleteOldNotificationReportGeneratorLogs, arg.Before, arg.NotificationTemplateID) return err } @@ -3765,24 +3765,24 @@ func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind No return items, nil } -const getReportGeneratorLogByUserAndTemplate = `-- name: GetReportGeneratorLogByUserAndTemplate :one +const GetNotificationReportGeneratorLogByUserAndTemplate = `-- name: GetNotificationReportGeneratorLogByUserAndTemplate :one SELECT user_id, notification_template_id, last_generated_at FROM - report_generator_logs + notification_report_generator_logs WHERE user_id = $1 AND notification_template_id = $2 ` -type GetReportGeneratorLogByUserAndTemplateParams struct { +type GetNotificationReportGeneratorLogByUserAndTemplateParams struct { UserID uuid.UUID `db:"user_id" json:"user_id"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` } // Fetch the report generator log indicating recent activity. -func (q *sqlQuerier) GetReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) { - row := q.db.QueryRowContext(ctx, getReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) +func (q *sqlQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) { + row := q.db.QueryRowContext(ctx, GetNotificationReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) var i ReportGeneratorLog err := row.Scan(&i.UserID, &i.NotificationTemplateID, &i.LastGeneratedAt) return i, err @@ -3876,21 +3876,21 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg return result.RowsAffected() } -const upsertReportGeneratorLog = `-- name: UpsertReportGeneratorLog :exec -INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) +const UpsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec +INSERT INTO notification_report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id +WHERE notification_report_generator_logs.user_id = EXCLUDED.user_id AND notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id ` -type UpsertReportGeneratorLogParams struct { +type UpsertNotificationReportGeneratorLogParams struct { UserID uuid.UUID `db:"user_id" json:"user_id"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` } // Insert or update report generator logs with recent activity. -func (q *sqlQuerier) UpsertReportGeneratorLog(ctx context.Context, arg UpsertReportGeneratorLogParams) error { - _, err := q.db.ExecContext(ctx, upsertReportGeneratorLog, arg.UserID, arg.NotificationTemplateID, arg.LastGeneratedAt) +func (q *sqlQuerier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error { + _, err := q.db.ExecContext(ctx, UpsertNotificationReportGeneratorLog, arg.UserID, arg.NotificationTemplateID, arg.LastGeneratedAt) return err } @@ -5924,7 +5924,7 @@ FROM provisioner_keys WHERE organization_id = $1 -AND +AND lower(name) = lower($2) ` @@ -7677,7 +7677,7 @@ func (q *sqlQuerier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUI } const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :exec -UPDATE +UPDATE tailnet_peers SET status = $2 diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 70554f281d115..3a65ad49adf85 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -175,8 +175,8 @@ FROM notification_templates WHERE kind = @kind::notification_template_kind ORDER BY name ASC; --- name: GetReportGeneratorLogByUserAndTemplate :one --- Fetch the report generator log indicating recent activity. +-- name: GetNotificationReportGeneratorLogByUserAndTemplate :one +-- Fetch the notification report generator log indicating recent activity. SELECT * FROM @@ -185,12 +185,12 @@ WHERE user_id = $1 AND notification_template_id = $2; --- name: UpsertReportGeneratorLog :exec --- Insert or update report generator logs with recent activity. +-- name: UpsertNotificationReportGeneratorLog :exec +-- Insert or update notification report generator logs with recent activity. INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; --- name: DeleteOldReportGeneratorLogs :exec +-- name: DeleteOldNotificationReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a @before date. DELETE FROM report_generator_logs WHERE last_generated_at < @before::timestamptz AND notification_template_id = @notification_template_id; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 927bb15bfda32..836aaf461d4de 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -46,7 +46,7 @@ const ( UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); - UniqueReportGeneratorLogsPkey UniqueConstraint = "report_generator_logs_pkey" // ALTER TABLE ONLY report_generator_logs ADD CONSTRAINT report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); + UniqueReportGeneratorLogsPkey UniqueConstraint = "notification_report_generator_logs_pkey" // ALTER TABLE ONLY notification_report_generator_logs ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id); diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 3115f5c6f2723..2a59e09e45a32 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -137,7 +137,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { - reportLog, err := db.GetReportGeneratorLogByUserAndTemplate(ctx, database.GetReportGeneratorLogByUserAndTemplateParams{ + reportLog, err := db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, database.GetNotificationReportGeneratorLogByUserAndTemplateParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }) @@ -177,7 +177,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for u := range processedUsers { - err = db.UpsertReportGeneratorLog(ctx, database.UpsertReportGeneratorLogParams{ + err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ UserID: u, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, LastGeneratedAt: dbtime.Time(now).UTC(), @@ -187,7 +187,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } - err = db.DeleteOldReportGeneratorLogs(ctx, database.DeleteOldReportGeneratorLogsParams{ + err = db.DeleteOldNotificationReportGeneratorLogs(ctx, database.DeleteOldNotificationReportGeneratorLogsParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, Before: dbtime.Time(now.Add(-failedWorkspaceBuildsReportFrequency - time.Hour)).UTC(), }) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts deleted file mode 100644 index 64bdb2d262852..0000000000000 --- a/site/src/api/typesGenerated.ts +++ /dev/null @@ -1,2377 +0,0 @@ -// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. - -// The code below is generated from codersdk. - -// From codersdk/templates.go -export interface ACLAvailable { - readonly users: Readonly>; - readonly groups: Readonly>; -} - -// From codersdk/apikey.go -export interface APIKey { - readonly id: string; - readonly user_id: string; - readonly last_used: string; - readonly expires_at: string; - readonly created_at: string; - readonly updated_at: string; - readonly login_type: LoginType; - readonly scope: APIKeyScope; - readonly token_name: string; - readonly lifetime_seconds: number; -} - -// From codersdk/apikey.go -export interface APIKeyWithOwner extends APIKey { - readonly username: string; -} - -// From codersdk/licenses.go -export interface AddLicenseRequest { - readonly license: string; -} - -// From codersdk/templates.go -export interface AgentStatsReportResponse { - readonly num_comms: number; - readonly rx_bytes: number; - readonly tx_bytes: number; -} - -// From codersdk/deployment.go -export interface AppHostResponse { - readonly host: string; -} - -// From codersdk/deployment.go -export interface AppearanceConfig { - readonly application_name: string; - readonly logo_url: string; - readonly service_banner: BannerConfig; - readonly announcement_banners: Readonly>; - readonly support_links?: Readonly>; -} - -// From codersdk/templates.go -export interface ArchiveTemplateVersionsRequest { - readonly all: boolean; -} - -// From codersdk/templates.go -export interface ArchiveTemplateVersionsResponse { - readonly template_id: string; - readonly archived_ids: Readonly>; -} - -// From codersdk/roles.go -export interface AssignableRoles extends Role { - readonly assignable: boolean; - readonly built_in: boolean; -} - -// From codersdk/audit.go -export type AuditDiff = Record - -// From codersdk/audit.go -export interface AuditDiffField { - // empty interface{} type, falling back to unknown - readonly old?: unknown; - // empty interface{} type, falling back to unknown - readonly new?: unknown; - readonly secret: boolean; -} - -// From codersdk/audit.go -export interface AuditLog { - readonly id: string; - readonly request_id: string; - readonly time: string; - readonly ip: string; - readonly user_agent: string; - readonly resource_type: ResourceType; - readonly resource_id: string; - readonly resource_target: string; - readonly resource_icon: string; - readonly action: AuditAction; - readonly diff: AuditDiff; - readonly status_code: number; - readonly additional_fields: Record; - readonly description: string; - readonly resource_link: string; - readonly is_deleted: boolean; - readonly organization_id: string; - readonly organization?: MinimalOrganization; - readonly user?: User; -} - -// From codersdk/audit.go -export interface AuditLogResponse { - readonly audit_logs: Readonly>; - readonly count: number; -} - -// From codersdk/audit.go -export interface AuditLogsRequest extends Pagination { - readonly q?: string; -} - -// From codersdk/users.go -export interface AuthMethod { - readonly enabled: boolean; -} - -// From codersdk/users.go -export interface AuthMethods { - readonly terms_of_service_url?: string; - readonly password: AuthMethod; - readonly github: AuthMethod; - readonly oidc: OIDCAuthMethod; -} - -// From codersdk/authorization.go -export interface AuthorizationCheck { - readonly object: AuthorizationObject; - readonly action: RBACAction; -} - -// From codersdk/authorization.go -export interface AuthorizationObject { - readonly resource_type: RBACResource; - readonly owner_id?: string; - readonly organization_id?: string; - readonly resource_id?: string; - readonly any_org?: boolean; -} - -// From codersdk/authorization.go -export interface AuthorizationRequest { - readonly checks: Record; -} - -// From codersdk/authorization.go -export type AuthorizationResponse = Record - -// From codersdk/deployment.go -export interface AvailableExperiments { - readonly safe: Readonly>; -} - -// From codersdk/deployment.go -export interface BannerConfig { - readonly enabled: boolean; - readonly message?: string; - readonly background_color?: string; -} - -// From codersdk/deployment.go -export interface BuildInfoResponse { - readonly external_url: string; - readonly version: string; - readonly dashboard_url: string; - readonly telemetry: boolean; - readonly workspace_proxy: boolean; - readonly agent_api_version: string; - readonly provisioner_api_version: string; - readonly upgrade_message: string; - readonly deployment_id: string; -} - -// From codersdk/insights.go -export interface ConnectionLatency { - readonly p50: number; - readonly p95: number; -} - -// From codersdk/users.go -export interface ConvertLoginRequest { - readonly to_type: LoginType; - readonly password: string; -} - -// From codersdk/users.go -export interface CreateFirstUserRequest { - readonly email: string; - readonly username: string; - readonly name: string; - readonly password: string; - readonly trial: boolean; - readonly trial_info: CreateFirstUserTrialInfo; -} - -// From codersdk/users.go -export interface CreateFirstUserResponse { - readonly user_id: string; - readonly organization_id: string; -} - -// From codersdk/users.go -export interface CreateFirstUserTrialInfo { - readonly first_name: string; - readonly last_name: string; - readonly phone_number: string; - readonly job_title: string; - readonly company_name: string; - readonly country: string; - readonly developers: string; -} - -// From codersdk/groups.go -export interface CreateGroupRequest { - readonly name: string; - readonly display_name: string; - readonly avatar_url: string; - readonly quota_allowance: number; -} - -// From codersdk/organizations.go -export interface CreateOrganizationRequest { - readonly name: string; - readonly display_name?: string; - readonly description?: string; - readonly icon?: string; -} - -// From codersdk/provisionerdaemons.go -export interface CreateProvisionerKeyRequest { - readonly name: string; - readonly tags: Record; -} - -// From codersdk/provisionerdaemons.go -export interface CreateProvisionerKeyResponse { - readonly key: string; -} - -// From codersdk/organizations.go -export interface CreateTemplateRequest { - readonly name: string; - readonly display_name?: string; - readonly description?: string; - readonly icon?: string; - readonly template_version_id: string; - readonly default_ttl_ms?: number; - readonly activity_bump_ms?: number; - readonly autostop_requirement?: TemplateAutostopRequirement; - readonly autostart_requirement?: TemplateAutostartRequirement; - readonly allow_user_cancel_workspace_jobs?: boolean; - readonly allow_user_autostart?: boolean; - readonly allow_user_autostop?: boolean; - readonly failure_ttl_ms?: number; - readonly dormant_ttl_ms?: number; - readonly delete_ttl_ms?: number; - readonly disable_everyone_group_access: boolean; - readonly require_active_version: boolean; - readonly max_port_share_level?: WorkspaceAgentPortShareLevel; -} - -// From codersdk/templateversions.go -export interface CreateTemplateVersionDryRunRequest { - readonly workspace_name: string; - readonly rich_parameter_values: Readonly>; - readonly user_variable_values?: Readonly>; -} - -// From codersdk/organizations.go -export interface CreateTemplateVersionRequest { - readonly name?: string; - readonly message?: string; - readonly template_id?: string; - readonly storage_method: ProvisionerStorageMethod; - readonly file_id?: string; - readonly example_id?: string; - readonly provisioner: ProvisionerType; - readonly tags: Record; - readonly user_variable_values?: Readonly>; -} - -// From codersdk/audit.go -export interface CreateTestAuditLogRequest { - readonly action?: AuditAction; - readonly resource_type?: ResourceType; - readonly resource_id?: string; - readonly additional_fields?: Record; - readonly time?: string; - readonly build_reason?: BuildReason; - readonly organization_id?: string; -} - -// From codersdk/apikey.go -export interface CreateTokenRequest { - readonly lifetime: number; - readonly scope: APIKeyScope; - readonly token_name: string; -} - -// From codersdk/users.go -export interface CreateUserRequestWithOrgs { - readonly email: string; - readonly username: string; - readonly name: string; - readonly password: string; - readonly login_type: LoginType; - readonly organization_ids: Readonly>; -} - -// From codersdk/workspaces.go -export interface CreateWorkspaceBuildRequest { - readonly template_version_id?: string; - readonly transition: WorkspaceTransition; - readonly dry_run?: boolean; - readonly state?: string; - readonly orphan?: boolean; - readonly rich_parameter_values?: Readonly>; - readonly log_level?: ProvisionerLogLevel; -} - -// From codersdk/workspaceproxy.go -export interface CreateWorkspaceProxyRequest { - readonly name: string; - readonly display_name: string; - readonly icon: string; -} - -// From codersdk/organizations.go -export interface CreateWorkspaceRequest { - readonly template_id?: string; - readonly template_version_id?: string; - readonly name: string; - readonly autostart_schedule?: string; - readonly ttl_ms?: number; - readonly rich_parameter_values?: Readonly>; - readonly automatic_updates?: AutomaticUpdates; -} - -// From codersdk/roles.go -export interface CustomRoleRequest { - readonly name: string; - readonly display_name: string; - readonly site_permissions: Readonly>; - readonly organization_permissions: Readonly>; - readonly user_permissions: Readonly>; -} - -// From codersdk/deployment.go -export interface DAUEntry { - readonly date: string; - readonly amount: number; -} - -// From codersdk/deployment.go -export interface DAURequest { - readonly TZHourOffset: number; -} - -// From codersdk/deployment.go -export interface DAUsResponse { - readonly entries: Readonly>; - readonly tz_hour_offset: number; -} - -// From codersdk/deployment.go -export interface DERP { - readonly server: DERPServerConfig; - readonly config: DERPConfig; -} - -// From codersdk/deployment.go -export interface DERPConfig { - readonly block_direct: boolean; - readonly force_websockets: boolean; - readonly url: string; - readonly path: string; -} - -// From codersdk/workspaceagents.go -export interface DERPRegion { - readonly preferred: boolean; - readonly latency_ms: number; -} - -// From codersdk/deployment.go -export interface DERPServerConfig { - readonly enable: boolean; - readonly region_id: number; - readonly region_code: string; - readonly region_name: string; - readonly stun_addresses: string[]; - readonly relay_url: string; -} - -// From codersdk/deployment.go -export interface DangerousConfig { - readonly allow_path_app_sharing: boolean; - readonly allow_path_app_site_owner_access: boolean; - readonly allow_all_cors: boolean; -} - -// From codersdk/workspaceagentportshare.go -export interface DeleteWorkspaceAgentPortShareRequest { - readonly agent_name: string; - readonly port: number; -} - -// From codersdk/deployment.go -export interface DeploymentConfig { - readonly config?: DeploymentValues; - readonly options?: SerpentOptionSet; -} - -// From codersdk/deployment.go -export interface DeploymentStats { - readonly aggregated_from: string; - readonly collected_at: string; - readonly next_update_at: string; - readonly workspaces: WorkspaceDeploymentStats; - readonly session_count: SessionCountDeploymentStats; -} - -// From codersdk/deployment.go -export interface DeploymentValues { - readonly verbose?: boolean; - readonly access_url?: string; - readonly wildcard_access_url?: string; - readonly docs_url?: string; - readonly redirect_to_access_url?: boolean; - readonly http_address?: string; - readonly autobuild_poll_interval?: number; - readonly job_hang_detector_interval?: number; - readonly derp?: DERP; - readonly prometheus?: PrometheusConfig; - readonly pprof?: PprofConfig; - readonly proxy_trusted_headers?: string[]; - readonly proxy_trusted_origins?: string[]; - readonly cache_directory?: string; - readonly in_memory_database?: boolean; - readonly pg_connection_url?: string; - readonly pg_auth?: string; - readonly oauth2?: OAuth2Config; - readonly oidc?: OIDCConfig; - readonly telemetry?: TelemetryConfig; - readonly tls?: TLSConfig; - readonly trace?: TraceConfig; - readonly secure_auth_cookie?: boolean; - readonly strict_transport_security?: number; - readonly strict_transport_security_options?: string[]; - readonly ssh_keygen_algorithm?: string; - readonly metrics_cache_refresh_interval?: number; - readonly agent_stat_refresh_interval?: number; - readonly agent_fallback_troubleshooting_url?: string; - readonly browser_only?: boolean; - readonly scim_api_key?: string; - readonly external_token_encryption_keys?: string[]; - readonly provisioner?: ProvisionerConfig; - readonly rate_limit?: RateLimitConfig; - readonly experiments?: string[]; - readonly update_check?: boolean; - readonly swagger?: SwaggerConfig; - readonly logging?: LoggingConfig; - readonly dangerous?: DangerousConfig; - readonly disable_path_apps?: boolean; - readonly session_lifetime?: SessionLifetime; - readonly disable_password_auth?: boolean; - readonly support?: SupportConfig; - readonly external_auth?: Readonly>; - readonly config_ssh?: SSHConfig; - readonly wgtunnel_host?: string; - readonly disable_owner_workspace_exec?: boolean; - readonly proxy_health_status_interval?: number; - readonly enable_terraform_debug_mode?: boolean; - readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig; - readonly web_terminal_renderer?: string; - readonly allow_workspace_renames?: boolean; - readonly healthcheck?: HealthcheckConfig; - readonly cli_upgrade_message?: string; - readonly terms_of_service_url?: string; - readonly notifications?: NotificationsConfig; - readonly config?: string; - readonly write_config?: boolean; - readonly address?: string; -} - -// From codersdk/deployment.go -export interface Entitlements { - readonly features: Record; - readonly warnings: Readonly>; - readonly errors: Readonly>; - readonly has_license: boolean; - readonly trial: boolean; - readonly require_telemetry: boolean; - readonly refreshed_at: string; -} - -// From codersdk/deployment.go -export type Experiments = Readonly> - -// From codersdk/externalauth.go -export interface ExternalAuth { - readonly authenticated: boolean; - readonly device: boolean; - readonly display_name: string; - readonly user?: ExternalAuthUser; - readonly app_installable: boolean; - readonly installations: Readonly>; - readonly app_install_url: string; -} - -// From codersdk/externalauth.go -export interface ExternalAuthAppInstallation { - readonly id: number; - readonly account: ExternalAuthUser; - readonly configure_url: string; -} - -// From codersdk/deployment.go -export interface ExternalAuthConfig { - readonly type: string; - readonly client_id: string; - readonly id: string; - readonly auth_url: string; - readonly token_url: string; - readonly validate_url: string; - readonly app_install_url: string; - readonly app_installations_url: string; - readonly no_refresh: boolean; - readonly scopes: Readonly>; - readonly device_flow: boolean; - readonly device_code_url: string; - readonly regex: string; - readonly display_name: string; - readonly display_icon: string; -} - -// From codersdk/externalauth.go -export interface ExternalAuthDevice { - readonly device_code: string; - readonly user_code: string; - readonly verification_uri: string; - readonly expires_in: number; - readonly interval: number; -} - -// From codersdk/externalauth.go -export interface ExternalAuthDeviceExchange { - readonly device_code: string; -} - -// From codersdk/externalauth.go -export interface ExternalAuthLink { - readonly provider_id: string; - readonly created_at: string; - readonly updated_at: string; - readonly has_refresh_token: boolean; - readonly expires: string; - readonly authenticated: boolean; - readonly validate_error: string; -} - -// From codersdk/externalauth.go -export interface ExternalAuthLinkProvider { - readonly id: string; - readonly type: string; - readonly device: boolean; - readonly display_name: string; - readonly display_icon: string; - readonly allow_refresh: boolean; - readonly allow_validate: boolean; -} - -// From codersdk/externalauth.go -export interface ExternalAuthUser { - readonly id: number; - readonly login: string; - readonly avatar_url: string; - readonly profile_url: string; - readonly name: string; -} - -// From codersdk/deployment.go -export interface Feature { - readonly entitlement: Entitlement; - readonly enabled: boolean; - readonly limit?: number; - readonly actual?: number; -} - -// From codersdk/apikey.go -export interface GenerateAPIKeyResponse { - readonly key: string; -} - -// From codersdk/users.go -export interface GetUsersResponse { - readonly users: Readonly>; - readonly count: number; -} - -// From codersdk/gitsshkey.go -export interface GitSSHKey { - readonly user_id: string; - readonly created_at: string; - readonly updated_at: string; - readonly public_key: string; -} - -// From codersdk/groups.go -export interface Group { - readonly id: string; - readonly name: string; - readonly display_name: string; - readonly organization_id: string; - readonly members: Readonly>; - readonly total_member_count: number; - readonly avatar_url: string; - readonly quota_allowance: number; - readonly source: GroupSource; - readonly organization_name: string; - readonly organization_display_name: string; -} - -// From codersdk/groups.go -export interface GroupArguments { - readonly Organization: string; - readonly HasMember: string; -} - -// From codersdk/workspaceapps.go -export interface Healthcheck { - readonly url: string; - readonly interval: number; - readonly threshold: number; -} - -// From codersdk/deployment.go -export interface HealthcheckConfig { - readonly refresh: number; - readonly threshold_database: number; -} - -// From codersdk/workspaceagents.go -export interface IssueReconnectingPTYSignedTokenRequest { - readonly url: string; - readonly agentID: string; -} - -// From codersdk/workspaceagents.go -export interface IssueReconnectingPTYSignedTokenResponse { - readonly signed_token: string; -} - -// From codersdk/jfrog.go -export interface JFrogXrayScan { - readonly workspace_id: string; - readonly agent_id: string; - readonly critical: number; - readonly high: number; - readonly medium: number; - readonly results_url: string; -} - -// From codersdk/licenses.go -export interface License { - readonly id: number; - readonly uuid: string; - readonly uploaded_at: string; - // empty interface{} type, falling back to unknown - readonly claims: Record; -} - -// From codersdk/deployment.go -export interface LinkConfig { - readonly name: string; - readonly target: string; - readonly icon: string; -} - -// From codersdk/externalauth.go -export interface ListUserExternalAuthResponse { - readonly providers: Readonly>; - readonly links: Readonly>; -} - -// From codersdk/deployment.go -export interface LoggingConfig { - readonly log_filter: string[]; - readonly human: string; - readonly json: string; - readonly stackdriver: string; -} - -// From codersdk/users.go -export interface LoginWithPasswordRequest { - readonly email: string; - readonly password: string; -} - -// From codersdk/users.go -export interface LoginWithPasswordResponse { - readonly session_token: string; -} - -// From codersdk/organizations.go -export interface MinimalOrganization { - readonly id: string; - readonly name: string; - readonly display_name: string; - readonly icon: string; -} - -// From codersdk/users.go -export interface MinimalUser { - readonly id: string; - readonly username: string; - readonly avatar_url: string; -} - -// From codersdk/notifications.go -export interface NotificationMethodsResponse { - readonly available: Readonly>; - readonly default: string; -} - -// From codersdk/notifications.go -export interface NotificationPreference { - readonly id: string; - readonly disabled: boolean; - readonly updated_at: string; -} - -// From codersdk/notifications.go -export interface NotificationTemplate { - readonly id: string; - readonly name: string; - readonly title_template: string; - readonly body_template: string; - readonly actions: string; - readonly group: string; - readonly method: string; - readonly kind: string; -} - -// From codersdk/deployment.go -export interface NotificationsConfig { - readonly max_send_attempts: number; - readonly retry_interval: number; - readonly sync_interval: number; - readonly sync_buffer_size: number; - readonly lease_period: number; - readonly lease_count: number; - readonly fetch_interval: number; - readonly method: string; - readonly dispatch_timeout: number; - readonly email: NotificationsEmailConfig; - readonly webhook: NotificationsWebhookConfig; -} - -// From codersdk/deployment.go -export interface NotificationsEmailAuthConfig { - readonly identity: string; - readonly username: string; - readonly password: string; - readonly password_file: string; -} - -// From codersdk/deployment.go -export interface NotificationsEmailConfig { - readonly from: string; - readonly smarthost: string; - readonly hello: string; - readonly auth: NotificationsEmailAuthConfig; - readonly tls: NotificationsEmailTLSConfig; - readonly force_tls: boolean; -} - -// From codersdk/deployment.go -export interface NotificationsEmailTLSConfig { - readonly start_tls: boolean; - readonly server_name: string; - readonly insecure_skip_verify: boolean; - readonly ca_file: string; - readonly cert_file: string; - readonly key_file: string; -} - -// From codersdk/notifications.go -export interface NotificationsSettings { - readonly notifier_paused: boolean; -} - -// From codersdk/deployment.go -export interface NotificationsWebhookConfig { - readonly endpoint: string; -} - -// From codersdk/oauth2.go -export interface OAuth2AppEndpoints { - readonly authorization: string; - readonly token: string; - readonly device_authorization: string; -} - -// From codersdk/deployment.go -export interface OAuth2Config { - readonly github: OAuth2GithubConfig; -} - -// From codersdk/deployment.go -export interface OAuth2GithubConfig { - readonly client_id: string; - readonly client_secret: string; - readonly allowed_orgs: string[]; - readonly allowed_teams: string[]; - readonly allow_signups: boolean; - readonly allow_everyone: boolean; - readonly enterprise_base_url: string; -} - -// From codersdk/oauth2.go -export interface OAuth2ProviderApp { - readonly id: string; - readonly name: string; - readonly callback_url: string; - readonly icon: string; - readonly endpoints: OAuth2AppEndpoints; -} - -// From codersdk/oauth2.go -export interface OAuth2ProviderAppFilter { - readonly user_id?: string; -} - -// From codersdk/oauth2.go -export interface OAuth2ProviderAppSecret { - readonly id: string; - readonly last_used_at?: string; - readonly client_secret_truncated: string; -} - -// From codersdk/oauth2.go -export interface OAuth2ProviderAppSecretFull { - readonly id: string; - readonly client_secret_full: string; -} - -// From codersdk/users.go -export interface OAuthConversionResponse { - readonly state_string: string; - readonly expires_at: string; - readonly to_type: LoginType; - readonly user_id: string; -} - -// From codersdk/users.go -export interface OIDCAuthMethod extends AuthMethod { - readonly signInText: string; - readonly iconUrl: string; -} - -// From codersdk/deployment.go -export interface OIDCConfig { - readonly allow_signups: boolean; - readonly client_id: string; - readonly client_secret: string; - readonly client_key_file: string; - readonly client_cert_file: string; - readonly email_domain: string[]; - readonly issuer_url: string; - readonly scopes: string[]; - readonly ignore_email_verified: boolean; - readonly username_field: string; - readonly name_field: string; - readonly email_field: string; - readonly auth_url_params: Record; - readonly ignore_user_info: boolean; - readonly organization_field: string; - readonly organization_mapping: Record>>; - readonly organization_assign_default: boolean; - readonly group_auto_create: boolean; - readonly group_regex_filter: string; - readonly group_allow_list: string[]; - readonly groups_field: string; - readonly group_mapping: Record; - readonly user_role_field: string; - readonly user_role_mapping: Record>>; - readonly user_roles_default: string[]; - readonly sign_in_text: string; - readonly icon_url: string; - readonly signups_disabled_text: string; - readonly skip_issuer_checks: boolean; -} - -// From codersdk/organizations.go -export interface Organization extends MinimalOrganization { - readonly description: string; - readonly created_at: string; - readonly updated_at: string; - readonly is_default: boolean; -} - -// From codersdk/organizations.go -export interface OrganizationMember { - readonly user_id: string; - readonly organization_id: string; - readonly created_at: string; - readonly updated_at: string; - readonly roles: Readonly>; -} - -// From codersdk/organizations.go -export interface OrganizationMemberWithUserData extends OrganizationMember { - readonly username: string; - readonly name: string; - readonly avatar_url: string; - readonly email: string; - readonly global_roles: Readonly>; -} - -// From codersdk/pagination.go -export interface Pagination { - readonly after_id?: string; - readonly limit?: number; - readonly offset?: number; -} - -// From codersdk/groups.go -export interface PatchGroupRequest { - readonly add_users: Readonly>; - readonly remove_users: Readonly>; - readonly name: string; - readonly display_name?: string; - readonly avatar_url?: string; - readonly quota_allowance?: number; -} - -// From codersdk/templateversions.go -export interface PatchTemplateVersionRequest { - readonly name: string; - readonly message?: string; -} - -// From codersdk/workspaceproxy.go -export interface PatchWorkspaceProxy { - readonly id: string; - readonly name: string; - readonly display_name: string; - readonly icon: string; - readonly regenerate_token: boolean; -} - -// From codersdk/roles.go -export interface Permission { - readonly negate: boolean; - readonly resource_type: RBACResource; - readonly action: RBACAction; -} - -// From codersdk/oauth2.go -export interface PostOAuth2ProviderAppRequest { - readonly name: string; - readonly callback_url: string; - readonly icon: string; -} - -// From codersdk/workspaces.go -export interface PostWorkspaceUsageRequest { - readonly agent_id: string; - readonly app_name: UsageAppName; -} - -// From codersdk/deployment.go -export interface PprofConfig { - readonly enable: boolean; - readonly address: string; -} - -// From codersdk/deployment.go -export interface PrometheusConfig { - readonly enable: boolean; - readonly address: string; - readonly collect_agent_stats: boolean; - readonly collect_db_metrics: boolean; - readonly aggregate_agent_stats_by: string[]; -} - -// From codersdk/deployment.go -export interface ProvisionerConfig { - readonly daemons: number; - readonly daemon_types: string[]; - readonly daemon_poll_interval: number; - readonly daemon_poll_jitter: number; - readonly force_cancel_interval: number; - readonly daemon_psk: string; -} - -// From codersdk/provisionerdaemons.go -export interface ProvisionerDaemon { - readonly id: string; - readonly organization_id: string; - readonly created_at: string; - readonly last_seen_at?: string; - readonly name: string; - readonly version: string; - readonly api_version: string; - readonly provisioners: Readonly>; - readonly tags: Record; -} - -// From codersdk/provisionerdaemons.go -export interface ProvisionerJob { - readonly id: string; - readonly created_at: string; - readonly started_at?: string; - readonly completed_at?: string; - readonly canceled_at?: string; - readonly error?: string; - readonly error_code?: JobErrorCode; - readonly status: ProvisionerJobStatus; - readonly worker_id?: string; - readonly file_id: string; - readonly tags: Record; - readonly queue_position: number; - readonly queue_size: number; -} - -// From codersdk/provisionerdaemons.go -export interface ProvisionerJobLog { - readonly id: number; - readonly created_at: string; - readonly log_source: LogSource; - readonly log_level: LogLevel; - readonly stage: string; - readonly output: string; -} - -// From codersdk/provisionerdaemons.go -export interface ProvisionerKey { - readonly id: string; - readonly created_at: string; - readonly organization: string; - readonly name: string; - readonly tags: Record; -} - -// From codersdk/workspaceproxy.go -export interface ProxyHealthReport { - readonly errors: Readonly>; - readonly warnings: Readonly>; -} - -// From codersdk/workspaces.go -export interface PutExtendWorkspaceRequest { - readonly deadline: string; -} - -// From codersdk/oauth2.go -export interface PutOAuth2ProviderAppRequest { - readonly name: string; - readonly callback_url: string; - readonly icon: string; -} - -// From codersdk/deployment.go -export interface RateLimitConfig { - readonly disable_all: boolean; - readonly api: number; -} - -// From codersdk/users.go -export interface ReducedUser extends MinimalUser { - readonly name: string; - readonly email: string; - readonly created_at: string; - readonly updated_at: string; - readonly last_seen_at: string; - readonly status: UserStatus; - readonly login_type: LoginType; - readonly theme_preference: string; -} - -// From codersdk/workspaceproxy.go -export interface Region { - readonly id: string; - readonly name: string; - readonly display_name: string; - readonly icon_url: string; - readonly healthy: boolean; - readonly path_app_url: string; - readonly wildcard_hostname: string; -} - -// From codersdk/workspaceproxy.go -export interface RegionsResponse { - readonly regions: Readonly>; -} - -// From codersdk/replicas.go -export interface Replica { - readonly id: string; - readonly hostname: string; - readonly created_at: string; - readonly relay_address: string; - readonly region_id: number; - readonly error: string; - readonly database_latency: number; -} - -// From codersdk/workspaces.go -export interface ResolveAutostartResponse { - readonly parameter_mismatch: boolean; -} - -// From codersdk/client.go -export interface Response { - readonly message: string; - readonly detail?: string; - readonly validations?: Readonly>; -} - -// From codersdk/roles.go -export interface Role { - readonly name: string; - readonly organization_id?: string; - readonly display_name: string; - readonly site_permissions: Readonly>; - readonly organization_permissions: Readonly>; - readonly user_permissions: Readonly>; -} - -// From codersdk/deployment.go -export interface SSHConfig { - readonly DeploymentName: string; - readonly SSHConfigOptions: string[]; -} - -// From codersdk/deployment.go -export interface SSHConfigResponse { - readonly hostname_prefix: string; - readonly ssh_config_options: Record; -} - -// From codersdk/serversentevents.go -export interface ServerSentEvent { - readonly type: ServerSentEventType; - // empty interface{} type, falling back to unknown - readonly data: unknown; -} - -// From codersdk/deployment.go -export interface ServiceBannerConfig { - readonly enabled: boolean; - readonly message?: string; - readonly background_color?: string; -} - -// From codersdk/deployment.go -export interface SessionCountDeploymentStats { - readonly vscode: number; - readonly ssh: number; - readonly jetbrains: number; - readonly reconnecting_pty: number; -} - -// From codersdk/deployment.go -export interface SessionLifetime { - readonly disable_expiry_refresh?: boolean; - readonly default_duration: number; - readonly max_token_lifetime?: number; -} - -// From codersdk/roles.go -export interface SlimRole { - readonly name: string; - readonly display_name: string; - readonly organization_id?: string; -} - -// From codersdk/deployment.go -export interface SupportConfig { - readonly links: Readonly>; -} - -// From codersdk/deployment.go -export interface SwaggerConfig { - readonly enable: boolean; -} - -// From codersdk/deployment.go -export interface TLSConfig { - readonly enable: boolean; - readonly address: string; - readonly redirect_http: boolean; - readonly cert_file: string[]; - readonly client_auth: string; - readonly client_ca_file: string; - readonly key_file: string[]; - readonly min_version: string; - readonly client_cert_file: string; - readonly client_key_file: string; - readonly supported_ciphers: string[]; - readonly allow_insecure_ciphers: boolean; -} - -// From codersdk/deployment.go -export interface TelemetryConfig { - readonly enable: boolean; - readonly trace: boolean; - readonly url: string; -} - -// From codersdk/templates.go -export interface Template { - readonly id: string; - readonly created_at: string; - readonly updated_at: string; - readonly organization_id: string; - readonly organization_name: string; - readonly organization_display_name: string; - readonly organization_icon: string; - readonly name: string; - readonly display_name: string; - readonly provisioner: ProvisionerType; - readonly active_version_id: string; - readonly active_user_count: number; - readonly build_time_stats: TemplateBuildTimeStats; - readonly description: string; - readonly deprecated: boolean; - readonly deprecation_message: string; - readonly icon: string; - readonly default_ttl_ms: number; - readonly activity_bump_ms: number; - readonly autostop_requirement: TemplateAutostopRequirement; - readonly autostart_requirement: TemplateAutostartRequirement; - readonly created_by_id: string; - readonly created_by_name: string; - readonly allow_user_autostart: boolean; - readonly allow_user_autostop: boolean; - readonly allow_user_cancel_workspace_jobs: boolean; - readonly failure_ttl_ms: number; - readonly time_til_dormant_ms: number; - readonly time_til_dormant_autodelete_ms: number; - readonly require_active_version: boolean; - readonly max_port_share_level: WorkspaceAgentPortShareLevel; -} - -// From codersdk/templates.go -export interface TemplateACL { - readonly users: Readonly>; - readonly group: Readonly>; -} - -// From codersdk/insights.go -export interface TemplateAppUsage { - readonly template_ids: Readonly>; - readonly type: TemplateAppsType; - readonly display_name: string; - readonly slug: string; - readonly icon: string; - readonly seconds: number; - readonly times_used: number; -} - -// From codersdk/templates.go -export interface TemplateAutostartRequirement { - readonly days_of_week: Readonly>; -} - -// From codersdk/templates.go -export interface TemplateAutostopRequirement { - readonly days_of_week: Readonly>; - readonly weeks: number; -} - -// From codersdk/templates.go -export type TemplateBuildTimeStats = Record - -// From codersdk/templates.go -export interface TemplateExample { - readonly id: string; - readonly url: string; - readonly name: string; - readonly description: string; - readonly icon: string; - readonly tags: Readonly>; - readonly markdown: string; -} - -// From codersdk/organizations.go -export interface TemplateFilter { - readonly q?: string; -} - -// From codersdk/templates.go -export interface TemplateGroup extends Group { - readonly role: TemplateRole; -} - -// From codersdk/insights.go -export interface TemplateInsightsIntervalReport { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; - readonly interval: InsightsReportInterval; - readonly active_users: number; -} - -// From codersdk/insights.go -export interface TemplateInsightsReport { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; - readonly active_users: number; - readonly apps_usage: Readonly>; - readonly parameters_usage: Readonly>; -} - -// From codersdk/insights.go -export interface TemplateInsightsRequest { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; - readonly interval: InsightsReportInterval; - readonly sections: Readonly>; -} - -// From codersdk/insights.go -export interface TemplateInsightsResponse { - readonly report?: TemplateInsightsReport; - readonly interval_reports?: Readonly>; -} - -// From codersdk/insights.go -export interface TemplateParameterUsage { - readonly template_ids: Readonly>; - readonly display_name: string; - readonly name: string; - readonly type: string; - readonly description: string; - readonly options?: Readonly>; - readonly values: Readonly>; -} - -// From codersdk/insights.go -export interface TemplateParameterValue { - readonly value: string; - readonly count: number; -} - -// From codersdk/templates.go -export interface TemplateUser extends User { - readonly role: TemplateRole; -} - -// From codersdk/templateversions.go -export interface TemplateVersion { - readonly id: string; - readonly template_id?: string; - readonly organization_id?: string; - readonly created_at: string; - readonly updated_at: string; - readonly name: string; - readonly message: string; - readonly job: ProvisionerJob; - readonly readme: string; - readonly created_by: MinimalUser; - readonly archived: boolean; - readonly warnings?: Readonly>; -} - -// From codersdk/templateversions.go -export interface TemplateVersionExternalAuth { - readonly id: string; - readonly type: string; - readonly display_name: string; - readonly display_icon: string; - readonly authenticate_url: string; - readonly authenticated: boolean; - readonly optional?: boolean; -} - -// From codersdk/templateversions.go -export interface TemplateVersionParameter { - readonly name: string; - readonly display_name?: string; - readonly description: string; - readonly description_plaintext: string; - readonly type: string; - readonly mutable: boolean; - readonly default_value: string; - readonly icon: string; - readonly options: Readonly>; - readonly validation_error?: string; - readonly validation_regex?: string; - readonly validation_min?: number; - readonly validation_max?: number; - readonly validation_monotonic?: ValidationMonotonicOrder; - readonly required: boolean; - readonly ephemeral: boolean; -} - -// From codersdk/templateversions.go -export interface TemplateVersionParameterOption { - readonly name: string; - readonly description: string; - readonly value: string; - readonly icon: string; -} - -// From codersdk/templateversions.go -export interface TemplateVersionVariable { - readonly name: string; - readonly description: string; - readonly type: string; - readonly value: string; - readonly default_value: string; - readonly required: boolean; - readonly sensitive: boolean; -} - -// From codersdk/templates.go -export interface TemplateVersionsByTemplateRequest extends Pagination { - readonly template_id: string; - readonly include_archived: boolean; -} - -// From codersdk/apikey.go -export interface TokenConfig { - readonly max_token_lifetime: number; -} - -// From codersdk/apikey.go -export interface TokensFilter { - readonly include_all: boolean; -} - -// From codersdk/deployment.go -export interface TraceConfig { - readonly enable: boolean; - readonly honeycomb_api_key: string; - readonly capture_logs: boolean; - readonly data_dog: boolean; -} - -// From codersdk/templates.go -export interface TransitionStats { - readonly P50?: number; - readonly P95?: number; -} - -// From codersdk/templates.go -export interface UpdateActiveTemplateVersion { - readonly id: string; -} - -// From codersdk/deployment.go -export interface UpdateAppearanceConfig { - readonly application_name: string; - readonly logo_url: string; - readonly service_banner: BannerConfig; - readonly announcement_banners: Readonly>; -} - -// From codersdk/updatecheck.go -export interface UpdateCheckResponse { - readonly current: boolean; - readonly version: string; - readonly url: string; -} - -// From codersdk/notifications.go -export interface UpdateNotificationTemplateMethod { - readonly method?: string; -} - -// From codersdk/organizations.go -export interface UpdateOrganizationRequest { - readonly name?: string; - readonly display_name?: string; - readonly description?: string; - readonly icon?: string; -} - -// From codersdk/users.go -export interface UpdateRoles { - readonly roles: Readonly>; -} - -// From codersdk/templates.go -export interface UpdateTemplateACL { - readonly user_perms?: Record; - readonly group_perms?: Record; -} - -// From codersdk/templates.go -export interface UpdateTemplateMeta { - readonly name?: string; - readonly display_name?: string; - readonly description?: string; - readonly icon?: string; - readonly default_ttl_ms?: number; - readonly activity_bump_ms?: number; - readonly autostop_requirement?: TemplateAutostopRequirement; - readonly autostart_requirement?: TemplateAutostartRequirement; - readonly allow_user_autostart?: boolean; - readonly allow_user_autostop?: boolean; - readonly allow_user_cancel_workspace_jobs?: boolean; - readonly failure_ttl_ms?: number; - readonly time_til_dormant_ms?: number; - readonly time_til_dormant_autodelete_ms?: number; - readonly update_workspace_last_used_at: boolean; - readonly update_workspace_dormant_at: boolean; - readonly require_active_version?: boolean; - readonly deprecation_message?: string; - readonly disable_everyone_group_access: boolean; - readonly max_port_share_level?: WorkspaceAgentPortShareLevel; -} - -// From codersdk/users.go -export interface UpdateUserAppearanceSettingsRequest { - readonly theme_preference: string; -} - -// From codersdk/notifications.go -export interface UpdateUserNotificationPreferences { - readonly template_disabled_map: Record; -} - -// From codersdk/users.go -export interface UpdateUserPasswordRequest { - readonly old_password: string; - readonly password: string; -} - -// From codersdk/users.go -export interface UpdateUserProfileRequest { - readonly username: string; - readonly name: string; -} - -// From codersdk/users.go -export interface UpdateUserQuietHoursScheduleRequest { - readonly schedule: string; -} - -// From codersdk/workspaces.go -export interface UpdateWorkspaceAutomaticUpdatesRequest { - readonly automatic_updates: AutomaticUpdates; -} - -// From codersdk/workspaces.go -export interface UpdateWorkspaceAutostartRequest { - readonly schedule?: string; -} - -// From codersdk/workspaces.go -export interface UpdateWorkspaceDormancy { - readonly dormant: boolean; -} - -// From codersdk/workspaceproxy.go -export interface UpdateWorkspaceProxyResponse { - readonly proxy: WorkspaceProxy; - readonly proxy_token: string; -} - -// From codersdk/workspaces.go -export interface UpdateWorkspaceRequest { - readonly name?: string; -} - -// From codersdk/workspaces.go -export interface UpdateWorkspaceTTLRequest { - readonly ttl_ms?: number; -} - -// From codersdk/files.go -export interface UploadResponse { - readonly hash: string; -} - -// From codersdk/workspaceagentportshare.go -export interface UpsertWorkspaceAgentPortShareRequest { - readonly agent_name: string; - readonly port: number; - readonly share_level: WorkspaceAgentPortShareLevel; - readonly protocol: WorkspaceAgentPortShareProtocol; -} - -// From codersdk/users.go -export interface User extends ReducedUser { - readonly organization_ids: Readonly>; - readonly roles: Readonly>; -} - -// From codersdk/insights.go -export interface UserActivity { - readonly template_ids: Readonly>; - readonly user_id: string; - readonly username: string; - readonly avatar_url: string; - readonly seconds: number; -} - -// From codersdk/insights.go -export interface UserActivityInsightsReport { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; - readonly users: Readonly>; -} - -// From codersdk/insights.go -export interface UserActivityInsightsRequest { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; -} - -// From codersdk/insights.go -export interface UserActivityInsightsResponse { - readonly report: UserActivityInsightsReport; -} - -// From codersdk/insights.go -export interface UserLatency { - readonly template_ids: Readonly>; - readonly user_id: string; - readonly username: string; - readonly avatar_url: string; - readonly latency_ms: ConnectionLatency; -} - -// From codersdk/insights.go -export interface UserLatencyInsightsReport { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; - readonly users: Readonly>; -} - -// From codersdk/insights.go -export interface UserLatencyInsightsRequest { - readonly start_time: string; - readonly end_time: string; - readonly template_ids: Readonly>; -} - -// From codersdk/insights.go -export interface UserLatencyInsightsResponse { - readonly report: UserLatencyInsightsReport; -} - -// From codersdk/users.go -export interface UserLoginType { - readonly login_type: LoginType; -} - -// From codersdk/users.go -export interface UserParameter { - readonly name: string; - readonly value: string; -} - -// From codersdk/deployment.go -export interface UserQuietHoursScheduleConfig { - readonly default_schedule: string; - readonly allow_user_custom: boolean; -} - -// From codersdk/users.go -export interface UserQuietHoursScheduleResponse { - readonly raw_schedule: string; - readonly user_set: boolean; - readonly user_can_set: boolean; - readonly time: string; - readonly timezone: string; - readonly next: string; -} - -// From codersdk/users.go -export interface UserRoles { - readonly roles: Readonly>; - readonly organization_roles: Record>>; -} - -// From codersdk/users.go -export interface UsersRequest extends Pagination { - readonly q?: string; -} - -// From codersdk/client.go -export interface ValidationError { - readonly field: string; - readonly detail: string; -} - -// From codersdk/organizations.go -export interface VariableValue { - readonly name: string; - readonly value: string; -} - -// From codersdk/workspaces.go -export interface Workspace { - readonly id: string; - readonly created_at: string; - readonly updated_at: string; - readonly owner_id: string; - readonly owner_name: string; - readonly owner_avatar_url: string; - readonly organization_id: string; - readonly organization_name: string; - readonly template_id: string; - readonly template_name: string; - readonly template_display_name: string; - readonly template_icon: string; - readonly template_allow_user_cancel_workspace_jobs: boolean; - readonly template_active_version_id: string; - readonly template_require_active_version: boolean; - readonly latest_build: WorkspaceBuild; - readonly outdated: boolean; - readonly name: string; - readonly autostart_schedule?: string; - readonly ttl_ms?: number; - readonly last_used_at: string; - readonly deleting_at?: string; - readonly dormant_at?: string; - readonly health: WorkspaceHealth; - readonly automatic_updates: AutomaticUpdates; - readonly allow_renames: boolean; - readonly favorite: boolean; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgent { - readonly id: string; - readonly created_at: string; - readonly updated_at: string; - readonly first_connected_at?: string; - readonly last_connected_at?: string; - readonly disconnected_at?: string; - readonly started_at?: string; - readonly ready_at?: string; - readonly status: WorkspaceAgentStatus; - readonly lifecycle_state: WorkspaceAgentLifecycle; - readonly name: string; - readonly resource_id: string; - readonly instance_id?: string; - readonly architecture: string; - readonly environment_variables: Record; - readonly operating_system: string; - readonly logs_length: number; - readonly logs_overflowed: boolean; - readonly directory?: string; - readonly expanded_directory?: string; - readonly version: string; - readonly api_version: string; - readonly apps: Readonly>; - readonly latency?: Record; - readonly connection_timeout_seconds: number; - readonly troubleshooting_url: string; - readonly subsystems: Readonly>; - readonly health: WorkspaceAgentHealth; - readonly display_apps: Readonly>; - readonly log_sources: Readonly>; - readonly scripts: Readonly>; - readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentHealth { - readonly healthy: boolean; - readonly reason?: string; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentListeningPort { - readonly process_name: string; - readonly network: string; - readonly port: number; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentListeningPortsResponse { - readonly ports: Readonly>; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentLog { - readonly id: number; - readonly created_at: string; - readonly output: string; - readonly level: LogLevel; - readonly source_id: string; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentLogSource { - readonly workspace_agent_id: string; - readonly id: string; - readonly created_at: string; - readonly display_name: string; - readonly icon: string; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentMetadata { - readonly result: WorkspaceAgentMetadataResult; - readonly description: WorkspaceAgentMetadataDescription; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentMetadataDescription { - readonly display_name: string; - readonly key: string; - readonly script: string; - readonly interval: number; - readonly timeout: number; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentMetadataResult { - readonly collected_at: string; - readonly age: number; - readonly value: string; - readonly error: string; -} - -// From codersdk/workspaceagentportshare.go -export interface WorkspaceAgentPortShare { - readonly workspace_id: string; - readonly agent_name: string; - readonly port: number; - readonly share_level: WorkspaceAgentPortShareLevel; - readonly protocol: WorkspaceAgentPortShareProtocol; -} - -// From codersdk/workspaceagentportshare.go -export interface WorkspaceAgentPortShares { - readonly shares: Readonly>; -} - -// From codersdk/workspaceagents.go -export interface WorkspaceAgentScript { - readonly log_source_id: string; - readonly log_path: string; - readonly script: string; - readonly cron: string; - readonly run_on_start: boolean; - readonly run_on_stop: boolean; - readonly start_blocks_login: boolean; - readonly timeout: number; -} - -// From codersdk/workspaceapps.go -export interface WorkspaceApp { - readonly id: string; - readonly url: string; - readonly external: boolean; - readonly slug: string; - readonly display_name: string; - readonly command?: string; - readonly icon?: string; - readonly subdomain: boolean; - readonly subdomain_name?: string; - readonly sharing_level: WorkspaceAppSharingLevel; - readonly healthcheck: Healthcheck; - readonly health: WorkspaceAppHealth; - readonly hidden: boolean; -} - -// From codersdk/workspacebuilds.go -export interface WorkspaceBuild { - readonly id: string; - readonly created_at: string; - readonly updated_at: string; - readonly workspace_id: string; - readonly workspace_name: string; - readonly workspace_owner_id: string; - readonly workspace_owner_name: string; - readonly workspace_owner_avatar_url: string; - readonly template_version_id: string; - readonly template_version_name: string; - readonly build_number: number; - readonly transition: WorkspaceTransition; - readonly initiator_id: string; - readonly initiator_name: string; - readonly job: ProvisionerJob; - readonly reason: BuildReason; - readonly resources: Readonly>; - readonly deadline?: string; - readonly max_deadline?: string; - readonly status: WorkspaceStatus; - readonly daily_cost: number; -} - -// From codersdk/workspacebuilds.go -export interface WorkspaceBuildParameter { - readonly name: string; - readonly value: string; -} - -// From codersdk/workspaces.go -export interface WorkspaceBuildsRequest extends Pagination { - readonly since?: string; -} - -// From codersdk/deployment.go -export interface WorkspaceConnectionLatencyMS { - readonly P50: number; - readonly P95: number; -} - -// From codersdk/deployment.go -export interface WorkspaceDeploymentStats { - readonly pending: number; - readonly building: number; - readonly running: number; - readonly failed: number; - readonly stopped: number; - readonly connection_latency_ms: WorkspaceConnectionLatencyMS; - readonly rx_bytes: number; - readonly tx_bytes: number; -} - -// From codersdk/workspaces.go -export interface WorkspaceFilter { - readonly q?: string; -} - -// From codersdk/workspaces.go -export interface WorkspaceHealth { - readonly healthy: boolean; - readonly failing_agents: Readonly>; -} - -// From codersdk/workspaces.go -export interface WorkspaceOptions { - readonly include_deleted?: boolean; -} - -// From codersdk/workspaceproxy.go -export interface WorkspaceProxy extends Region { - readonly derp_enabled: boolean; - readonly derp_only: boolean; - readonly status?: WorkspaceProxyStatus; - readonly created_at: string; - readonly updated_at: string; - readonly deleted: boolean; - readonly version: string; -} - -// From codersdk/deployment.go -export interface WorkspaceProxyBuildInfo { - readonly workspace_proxy: boolean; - readonly dashboard_url: string; -} - -// From codersdk/workspaceproxy.go -export interface WorkspaceProxyStatus { - readonly status: ProxyHealthStatus; - readonly report?: ProxyHealthReport; - readonly checked_at: string; -} - -// From codersdk/workspaces.go -export interface WorkspaceQuota { - readonly credits_consumed: number; - readonly budget: number; -} - -// From codersdk/workspacebuilds.go -export interface WorkspaceResource { - readonly id: string; - readonly created_at: string; - readonly job_id: string; - readonly workspace_transition: WorkspaceTransition; - readonly type: string; - readonly name: string; - readonly hide: boolean; - readonly icon: string; - readonly agents?: Readonly>; - readonly metadata?: Readonly>; - readonly daily_cost: number; -} - -// From codersdk/workspacebuilds.go -export interface WorkspaceResourceMetadata { - readonly key: string; - readonly value: string; - readonly sensitive: boolean; -} - -// From codersdk/workspaces.go -export interface WorkspacesRequest extends Pagination { - readonly q?: string; -} - -// From codersdk/workspaces.go -export interface WorkspacesResponse { - readonly workspaces: Readonly>; - readonly count: number; -} - -// From codersdk/apikey.go -export type APIKeyScope = "all" | "application_connect" -export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] - -// From codersdk/workspaceagents.go -export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace" -export const AgentSubsystems: AgentSubsystem[] = ["envbox", "envbuilder", "exectrace"] - -// From codersdk/audit.go -export type AuditAction = "create" | "delete" | "login" | "logout" | "register" | "start" | "stop" | "write" -export const AuditActions: AuditAction[] = ["create", "delete", "login", "logout", "register", "start", "stop", "write"] - -// From codersdk/workspaces.go -export type AutomaticUpdates = "always" | "never" -export const AutomaticUpdateses: AutomaticUpdates[] = ["always", "never"] - -// From codersdk/workspacebuilds.go -export type BuildReason = "autostart" | "autostop" | "initiator" -export const BuildReasons: BuildReason[] = ["autostart", "autostop", "initiator"] - -// From codersdk/workspaceagents.go -export type DisplayApp = "port_forwarding_helper" | "ssh_helper" | "vscode" | "vscode_insiders" | "web_terminal" -export const DisplayApps: DisplayApp[] = ["port_forwarding_helper", "ssh_helper", "vscode", "vscode_insiders", "web_terminal"] - -// From codersdk/externalauth.go -export type EnhancedExternalAuthProvider = "azure-devops" | "azure-devops-entra" | "bitbucket-cloud" | "bitbucket-server" | "gitea" | "github" | "gitlab" | "jfrog" | "slack" -export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = ["azure-devops", "azure-devops-entra", "bitbucket-cloud", "bitbucket-server", "gitea", "github", "gitlab", "jfrog", "slack"] - -// From codersdk/deployment.go -export type Entitlement = "entitled" | "grace_period" | "not_entitled" -export const Entitlements: Entitlement[] = ["entitled", "grace_period", "not_entitled"] - -// From codersdk/deployment.go -export type Experiment = "auto-fill-parameters" | "custom-roles" | "example" | "multi-organization" | "notifications" | "workspace-usage" -export const Experiments: Experiment[] = ["auto-fill-parameters", "custom-roles", "example", "multi-organization", "notifications", "workspace-usage"] - -// From codersdk/deployment.go -export type FeatureName = "access_control" | "advanced_template_scheduling" | "appearance" | "audit_log" | "browser_only" | "control_shared_ports" | "custom_roles" | "external_provisioner_daemons" | "external_token_encryption" | "high_availability" | "multiple_external_auth" | "multiple_organizations" | "scim" | "template_rbac" | "user_limit" | "user_role_management" | "workspace_batch_actions" | "workspace_proxy" -export const FeatureNames: FeatureName[] = ["access_control", "advanced_template_scheduling", "appearance", "audit_log", "browser_only", "control_shared_ports", "custom_roles", "external_provisioner_daemons", "external_token_encryption", "high_availability", "multiple_external_auth", "multiple_organizations", "scim", "template_rbac", "user_limit", "user_role_management", "workspace_batch_actions", "workspace_proxy"] - -// From codersdk/deployment.go -export type FeatureSet = "" | "enterprise" | "premium" -export const FeatureSets: FeatureSet[] = ["", "enterprise", "premium"] - -// From codersdk/groups.go -export type GroupSource = "oidc" | "user" -export const GroupSources: GroupSource[] = ["oidc", "user"] - -// From codersdk/insights.go -export type InsightsReportInterval = "day" | "week" -export const InsightsReportIntervals: InsightsReportInterval[] = ["day", "week"] - -// From codersdk/provisionerdaemons.go -export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES" -export const JobErrorCodes: JobErrorCode[] = ["REQUIRED_TEMPLATE_VARIABLES"] - -// From codersdk/provisionerdaemons.go -export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" -export const LogLevels: LogLevel[] = ["debug", "error", "info", "trace", "warn"] - -// From codersdk/provisionerdaemons.go -export type LogSource = "provisioner" | "provisioner_daemon" -export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"] - -// From codersdk/apikey.go -export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token" -export const LoginTypes: LoginType[] = ["", "github", "none", "oidc", "password", "token"] - -// From codersdk/oauth2.go -export type OAuth2ProviderGrantType = "authorization_code" | "refresh_token" -export const OAuth2ProviderGrantTypes: OAuth2ProviderGrantType[] = ["authorization_code", "refresh_token"] - -// From codersdk/oauth2.go -export type OAuth2ProviderResponseType = "code" -export const OAuth2ProviderResponseTypes: OAuth2ProviderResponseType[] = ["code"] - -// From codersdk/deployment.go -export type PostgresAuth = "awsiamrds" | "password" -export const PostgresAuths: PostgresAuth[] = ["awsiamrds", "password"] - -// From codersdk/provisionerdaemons.go -export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded" | "unknown" -export const ProvisionerJobStatuses: ProvisionerJobStatus[] = ["canceled", "canceling", "failed", "pending", "running", "succeeded", "unknown"] - -// From codersdk/workspaces.go -export type ProvisionerLogLevel = "debug" -export const ProvisionerLogLevels: ProvisionerLogLevel[] = ["debug"] - -// From codersdk/organizations.go -export type ProvisionerStorageMethod = "file" -export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ["file"] - -// From codersdk/organizations.go -export type ProvisionerType = "echo" | "terraform" -export const ProvisionerTypes: ProvisionerType[] = ["echo", "terraform"] - -// From codersdk/workspaceproxy.go -export type ProxyHealthStatus = "ok" | "unhealthy" | "unreachable" | "unregistered" -export const ProxyHealthStatuses: ProxyHealthStatus[] = ["ok", "unhealthy", "unreachable", "unregistered"] - -// From codersdk/rbacresources_gen.go -export type RBACAction = "application_connect" | "assign" | "create" | "delete" | "read" | "read_personal" | "ssh" | "start" | "stop" | "update" | "update_personal" | "use" | "view_insights" -export const RBACActions: RBACAction[] = ["application_connect", "assign", "create", "delete", "read", "read_personal", "ssh", "start", "stop", "update", "update_personal", "use", "view_insights"] - -// From codersdk/rbacresources_gen.go -export type RBACResource = "*" | "api_key" | "assign_org_role" | "assign_role" | "audit_log" | "debug_info" | "deployment_config" | "deployment_stats" | "file" | "group" | "group_member" | "license" | "notification_preference" | "notification_template" | "oauth2_app" | "oauth2_app_code_token" | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" | "template" | "user" | "workspace" | "workspace_dormant" | "workspace_proxy" -export const RBACResources: RBACResource[] = ["*", "api_key", "assign_org_role", "assign_role", "audit_log", "debug_info", "deployment_config", "deployment_stats", "file", "group", "group_member", "license", "notification_preference", "notification_template", "oauth2_app", "oauth2_app_code_token", "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", "workspace_dormant", "workspace_proxy"] - -// From codersdk/audit.go -export type ResourceType = "api_key" | "convert_login" | "custom_role" | "git_ssh_key" | "group" | "health_settings" | "license" | "notifications_settings" | "oauth2_provider_app" | "oauth2_provider_app_secret" | "organization" | "template" | "template_version" | "user" | "workspace" | "workspace_build" | "workspace_proxy" -export const ResourceTypes: ResourceType[] = ["api_key", "convert_login", "custom_role", "git_ssh_key", "group", "health_settings", "license", "notifications_settings", "oauth2_provider_app", "oauth2_provider_app_secret", "organization", "template", "template_version", "user", "workspace", "workspace_build", "workspace_proxy"] - -// From codersdk/serversentevents.go -export type ServerSentEventType = "data" | "error" | "ping" -export const ServerSentEventTypes: ServerSentEventType[] = ["data", "error", "ping"] - -// From codersdk/insights.go -export type TemplateAppsType = "app" | "builtin" -export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"] - -// From codersdk/insights.go -export type TemplateInsightsSection = "interval_reports" | "report" -export const TemplateInsightsSections: TemplateInsightsSection[] = ["interval_reports", "report"] - -// From codersdk/templates.go -export type TemplateRole = "" | "admin" | "use" -export const TemplateRoles: TemplateRole[] = ["", "admin", "use"] - -// From codersdk/templateversions.go -export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES" -export const TemplateVersionWarnings: TemplateVersionWarning[] = ["UNSUPPORTED_WORKSPACES"] - -// From codersdk/workspaces.go -export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode" -export const UsageAppNames: UsageAppName[] = ["jetbrains", "reconnecting-pty", "ssh", "vscode"] - -// From codersdk/users.go -export type UserStatus = "active" | "dormant" | "suspended" -export const UserStatuses: UserStatus[] = ["active", "dormant", "suspended"] - -// From codersdk/templateversions.go -export type ValidationMonotonicOrder = "decreasing" | "increasing" -export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = ["decreasing", "increasing"] - -// From codersdk/workspaceagents.go -export type WorkspaceAgentLifecycle = "created" | "off" | "ready" | "shutdown_error" | "shutdown_timeout" | "shutting_down" | "start_error" | "start_timeout" | "starting" -export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = ["created", "off", "ready", "shutdown_error", "shutdown_timeout", "shutting_down", "start_error", "start_timeout", "starting"] - -// From codersdk/workspaceagentportshare.go -export type WorkspaceAgentPortShareLevel = "authenticated" | "owner" | "public" -export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = ["authenticated", "owner", "public"] - -// From codersdk/workspaceagentportshare.go -export type WorkspaceAgentPortShareProtocol = "http" | "https" -export const WorkspaceAgentPortShareProtocols: WorkspaceAgentPortShareProtocol[] = ["http", "https"] - -// From codersdk/workspaceagents.go -export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking" -export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] = ["blocking", "non-blocking"] - -// From codersdk/workspaceagents.go -export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" | "timeout" -export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = ["connected", "connecting", "disconnected", "timeout"] - -// From codersdk/workspaceapps.go -export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy" -export const WorkspaceAppHealths: WorkspaceAppHealth[] = ["disabled", "healthy", "initializing", "unhealthy"] - -// From codersdk/workspaceapps.go -export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" -export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = ["authenticated", "owner", "public"] - -// From codersdk/workspacebuilds.go -export type WorkspaceStatus = "canceled" | "canceling" | "deleted" | "deleting" | "failed" | "pending" | "running" | "starting" | "stopped" | "stopping" -export const WorkspaceStatuses: WorkspaceStatus[] = ["canceled", "canceling", "deleted", "deleting", "failed", "pending", "running", "starting", "stopped", "stopping"] - -// From codersdk/workspacebuilds.go -export type WorkspaceTransition = "delete" | "start" | "stop" -export const WorkspaceTransitions: WorkspaceTransition[] = ["delete", "start", "stop"] - -// From codersdk/workspaceproxy.go -export type RegionTypes = Region | WorkspaceProxy - -// The code below is generated from codersdk/healthsdk. - -// From healthsdk/healthsdk.go -export interface AccessURLReport extends BaseReport { - readonly healthy: boolean; - readonly access_url: string; - readonly reachable: boolean; - readonly status_code: number; - readonly healthz_response: string; -} - -// From healthsdk/healthsdk.go -export interface BaseReport { - readonly error?: string; - readonly severity: HealthSeverity; - readonly warnings: Readonly>; - readonly dismissed: boolean; -} - -// From healthsdk/healthsdk.go -export interface DERPHealthReport extends BaseReport { - readonly healthy: boolean; - readonly regions: Record; - // TODO: narrow this type - readonly netcheck?: any; - readonly netcheck_err?: string; - readonly netcheck_logs: Readonly>; -} - -// From healthsdk/healthsdk.go -export interface DERPNodeReport { - readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly warnings: Readonly>; - readonly error?: string; - // TODO: narrow this type - readonly node?: any; - // TODO: narrow this type - readonly node_info: any; - readonly can_exchange_messages: boolean; - readonly round_trip_ping: string; - readonly round_trip_ping_ms: number; - readonly uses_websocket: boolean; - readonly client_logs: Readonly>>>; - readonly client_errs: Readonly>>>; - readonly stun: STUNReport; -} - -// From healthsdk/healthsdk.go -export interface DERPRegionReport { - readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly warnings: Readonly>; - readonly error?: string; - // TODO: narrow this type - readonly region?: any; - readonly node_reports: Readonly>; -} - -// From healthsdk/healthsdk.go -export interface DatabaseReport extends BaseReport { - readonly healthy: boolean; - readonly reachable: boolean; - readonly latency: string; - readonly latency_ms: number; - readonly threshold_ms: number; -} - -// From healthsdk/healthsdk.go -export interface HealthSettings { - readonly dismissed_healthchecks: Readonly>; -} - -// From healthsdk/healthsdk.go -export interface HealthcheckReport { - readonly time: string; - readonly healthy: boolean; - readonly severity: HealthSeverity; - readonly derp: DERPHealthReport; - readonly access_url: AccessURLReport; - readonly websocket: WebsocketReport; - readonly database: DatabaseReport; - readonly workspace_proxy: WorkspaceProxyReport; - readonly provisioner_daemons: ProvisionerDaemonsReport; - readonly coder_version: string; -} - -// From healthsdk/healthsdk.go -export interface ProvisionerDaemonsReport extends BaseReport { - readonly items: Readonly>; -} - -// From healthsdk/healthsdk.go -export interface ProvisionerDaemonsReportItem { - readonly provisioner_daemon: ProvisionerDaemon; - readonly warnings: Readonly>; -} - -// From healthsdk/healthsdk.go -export interface STUNReport { - readonly Enabled: boolean; - readonly CanSTUN: boolean; - readonly Error?: string; -} - -// From healthsdk/healthsdk.go -export interface UpdateHealthSettings { - readonly dismissed_healthchecks: Readonly>; -} - -// From healthsdk/healthsdk.go -export interface WebsocketReport extends BaseReport { - readonly healthy: boolean; - readonly body: string; - readonly code: number; -} - -// From healthsdk/healthsdk.go -export interface WorkspaceProxyReport extends BaseReport { - readonly healthy: boolean; - readonly workspace_proxies: RegionsResponse; -} - -// From healthsdk/healthsdk.go -export type HealthSection = "AccessURL" | "DERP" | "Database" | "ProvisionerDaemons" | "Websocket" | "WorkspaceProxy" -export const HealthSections: HealthSection[] = ["AccessURL", "DERP", "Database", "ProvisionerDaemons", "Websocket", "WorkspaceProxy"] - -// The code below is generated from coderd/healthcheck/health. - -// From health/model.go -export interface HealthMessage { - readonly code: HealthCode; - readonly message: string; -} - -// From health/model.go -export type HealthCode = "EACS01" | "EACS02" | "EACS03" | "EACS04" | "EDB01" | "EDB02" | "EDERP01" | "EDERP02" | "EPD01" | "EPD02" | "EPD03" | "EUNKNOWN" | "EWP01" | "EWP02" | "EWP04" | "EWS01" | "EWS02" | "EWS03" -export const HealthCodes: HealthCode[] = ["EACS01", "EACS02", "EACS03", "EACS04", "EDB01", "EDB02", "EDERP01", "EDERP02", "EPD01", "EPD02", "EPD03", "EUNKNOWN", "EWP01", "EWP02", "EWP04", "EWS01", "EWS02", "EWS03"] - -// From health/model.go -export type HealthSeverity = "error" | "ok" | "warning" -export const HealthSeveritys: HealthSeverity[] = ["error", "ok", "warning"] - -// The code below is generated from github.com/coder/serpent. - -// From serpent/serpent.go -export type SerpentAnnotations = Record - -// From serpent/serpent.go -export interface SerpentGroup { - readonly parent?: SerpentGroup; - readonly name?: string; - readonly yaml?: string; - readonly description?: string; -} - -// From serpent/option.go -export interface SerpentOption { - readonly name?: string; - readonly description?: string; - readonly required?: boolean; - readonly flag?: string; - readonly flag_shorthand?: string; - readonly env?: string; - readonly yaml?: string; - readonly default?: string; - // TODO: narrow this type - readonly value?: any; - readonly annotations?: SerpentAnnotations; - readonly group?: SerpentGroup; - readonly use_instead?: Readonly>; - readonly hidden?: boolean; - readonly value_source?: SerpentValueSource; -} - -// From serpent/option.go -export type SerpentOptionSet = Readonly> - -// From serpent/option.go -export type SerpentValueSource = "" | "default" | "env" | "flag" | "yaml" -export const SerpentValueSources: SerpentValueSource[] = ["", "default", "env", "flag", "yaml"] - From 832bf385d017ad81864acb3670dbc66c57e02d03 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 08:52:20 +0000 Subject: [PATCH 099/122] notifications --- coderd/database/dbauthz/dbauthz.go | 36 +- coderd/database/dbmem/dbmem.go | 102 +- coderd/database/dbmetrics/dbmetrics.go | 40 +- coderd/database/dbmock/dbmock.go | 82 +- coderd/database/querier.go | 12 +- coderd/database/queries.sql.go | 70 +- coderd/database/unique_constraint.go | 2 +- site/src/api/typesGenerated.ts | 2377 ++++++++++++++++++++++++ 8 files changed, 2549 insertions(+), 172 deletions(-) create mode 100644 site/src/api/typesGenerated.ts diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 08dc126f3b3a4..20d81c8bbb5d4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1137,18 +1137,18 @@ func (q *querier) DeleteOldNotificationMessages(ctx context.Context) error { return q.db.DeleteOldNotificationMessages(ctx) } -func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { +func (q *querier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg database.DeleteOldNotificationReportGeneratorLogsParams) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } - return q.db.DeleteOldProvisionerDaemons(ctx) + return q.db.DeleteOldNotificationReportGeneratorLogs(ctx, arg) } -func (q *querier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg database.DeleteOldNotificationReportGeneratorLogsParams) error { +func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err } - return q.db.DeleteOldNotificationReportGeneratorLogs(ctx, arg) + return q.db.DeleteOldProvisionerDaemons(ctx) } func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) error { @@ -1613,6 +1613,13 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab return q.db.GetNotificationMessagesByStatus(ctx, arg) } +func (q *querier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.ReportGeneratorLog{}, err + } + return q.db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) +} + func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil { return database.NotificationTemplate{}, err @@ -1877,13 +1884,6 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti return q.db.GetReplicasUpdatedAfter(ctx, updatedAt) } -func (q *querier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { - return database.ReportGeneratorLog{}, err - } - return q.db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) -} - func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err @@ -3939,6 +3939,13 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error { return q.db.UpsertLogoURL(ctx, value) } +func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpsertNotificationReportGeneratorLog(ctx, arg) +} + func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil { return err @@ -3964,13 +3971,6 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse return q.db.UpsertProvisionerDaemon(ctx, arg) } -func (q *querier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { - return err - } - return q.db.UpsertNotificationReportGeneratorLog(ctx, arg) -} - func (q *querier) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index e31203161607e..8c8617841417a 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1690,6 +1690,20 @@ func (*FakeQuerier) DeleteOldNotificationMessages(_ context.Context) error { return nil } +func (q *FakeQuerier) DeleteOldNotificationReportGeneratorLogs(_ context.Context, params database.DeleteOldNotificationReportGeneratorLogsParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + var validLogs []database.ReportGeneratorLog + for _, record := range q.reportGeneratorLogs { + if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.After(params.Before) { + validLogs = append(validLogs, record) + } + } + q.reportGeneratorLogs = validLogs + return nil +} + func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -1709,20 +1723,6 @@ func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { return nil } -func (q *FakeQuerier) DeleteOldNotificationReportGeneratorLogs(_ context.Context, params database.DeleteOldNotificationReportGeneratorLogsParams) error { - q.mutex.Lock() - defer q.mutex.Unlock() - - var validLogs []database.ReportGeneratorLog - for _, record := range q.reportGeneratorLogs { - if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.After(params.Before) { - validLogs = append(validLogs, record) - } - } - q.reportGeneratorLogs = validLogs - return nil -} - func (q *FakeQuerier) DeleteOldWorkspaceAgentLogs(_ context.Context, threshold time.Time) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -3002,6 +3002,23 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat return out, nil } +func (q *FakeQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.ReportGeneratorLog{}, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, record := range q.reportGeneratorLogs { + if record.UserID == arg.UserID && record.NotificationTemplateID == arg.NotificationTemplateID { + return record, nil + } + } + return database.ReportGeneratorLog{}, sql.ErrNoRows +} + func (*FakeQuerier) GetNotificationTemplateByID(_ context.Context, _ uuid.UUID) (database.NotificationTemplate, error) { // Not implementing this function because it relies on state in the database which is created with migrations. // We could consider using code-generation to align the database state and dbmem, but it's not worth it right now. @@ -3603,23 +3620,6 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time. return replicas, nil } -func (q *FakeQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.ReportGeneratorLog{}, err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, record := range q.reportGeneratorLogs { - if record.UserID == arg.UserID && record.NotificationTemplateID == arg.NotificationTemplateID { - return record, nil - } - } - return database.ReportGeneratorLog{}, sql.ErrNoRows -} - func (q *FakeQuerier) GetRuntimeConfig(_ context.Context, key string) (string, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -9374,6 +9374,26 @@ func (q *FakeQuerier) UpsertLogoURL(_ context.Context, data string) error { return nil } +func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, record := range q.reportGeneratorLogs { + if arg.NotificationTemplateID == record.NotificationTemplateID && arg.UserID == record.UserID { + q.reportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt + return nil + } + } + + q.reportGeneratorLogs = append(q.reportGeneratorLogs, database.ReportGeneratorLog(arg)) + return nil +} + func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -9429,26 +9449,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up return d, nil } -func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { - err := validateDatabaseType(arg) - if err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, record := range q.reportGeneratorLogs { - if arg.NotificationTemplateID == record.NotificationTemplateID && arg.UserID == record.UserID { - q.reportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt - return nil - } - } - - q.reportGeneratorLogs = append(q.reportGeneratorLogs, database.ReportGeneratorLog(arg)) - return nil -} - func (q *FakeQuerier) UpsertRuntimeConfig(_ context.Context, arg database.UpsertRuntimeConfigParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index f9a73c96c78b8..55ff9f01fde26 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -298,17 +298,17 @@ func (m metricsStore) DeleteOldNotificationMessages(ctx context.Context) error { return r0 } -func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error { +func (m metricsStore) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldNotificationReportGeneratorLogsParams) error { start := time.Now() - r0 := m.s.DeleteOldProvisionerDaemons(ctx) - m.queryLatencies.WithLabelValues("DeleteOldProvisionerDaemons").Observe(time.Since(start).Seconds()) + r0 := m.s.DeleteOldNotificationReportGeneratorLogs(ctx, frequencyDays) + m.queryLatencies.WithLabelValues("DeleteOldNotificationReportGeneratorLogs").Observe(time.Since(start).Seconds()) return r0 } -func (m metricsStore) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldNotificationReportGeneratorLogsParams) error { +func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error { start := time.Now() - r0 := m.s.DeleteOldNotificationReportGeneratorLogs(ctx, frequencyDays) - m.queryLatencies.WithLabelValues("DeleteOldNotificationReportGeneratorLogs").Observe(time.Since(start).Seconds()) + r0 := m.s.DeleteOldProvisionerDaemons(ctx) + m.queryLatencies.WithLabelValues("DeleteOldProvisionerDaemons").Observe(time.Since(start).Seconds()) return r0 } @@ -774,6 +774,13 @@ func (m metricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg d return r0, r1 } +func (m metricsStore) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + start := time.Now() + r0, r1 := m.s.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) + m.queryLatencies.WithLabelValues("GetNotificationReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) { start := time.Now() r0, r1 := m.s.GetNotificationTemplateByID(ctx, id) @@ -1012,13 +1019,6 @@ func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt tim return replicas, err } -func (m metricsStore) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { - start := time.Now() - r0, r1 := m.s.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) - m.queryLatencies.WithLabelValues("GetNotificationReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m metricsStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) { start := time.Now() r0, r1 := m.s.GetRuntimeConfig(ctx, key) @@ -2489,6 +2489,13 @@ func (m metricsStore) UpsertLogoURL(ctx context.Context, value string) error { return r0 } +func (m metricsStore) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { + start := time.Now() + r0 := m.s.UpsertNotificationReportGeneratorLog(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertNotificationReportGeneratorLog").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) UpsertNotificationsSettings(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertNotificationsSettings(ctx, value) @@ -2510,13 +2517,6 @@ func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database. return r0, r1 } -func (m metricsStore) UpsertNotificationReportGeneratorLog(ctx context.Context, arg database.UpsertNotificationReportGeneratorLogParams) error { - start := time.Now() - r0 := m.s.UpsertNotificationReportGeneratorLog(ctx, arg) - m.queryLatencies.WithLabelValues("UpsertNotificationReportGeneratorLog").Observe(time.Since(start).Seconds()) - return r0 -} - func (m metricsStore) UpsertRuntimeConfig(ctx context.Context, arg database.UpsertRuntimeConfigParams) error { start := time.Now() r0 := m.s.UpsertRuntimeConfig(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 99f23d99433c2..da6a0a866c5fb 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -486,32 +486,32 @@ func (mr *MockStoreMockRecorder) DeleteOldNotificationMessages(arg0 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNotificationMessages", reflect.TypeOf((*MockStore)(nil).DeleteOldNotificationMessages), arg0) } -// DeleteOldProvisionerDaemons mocks base method. -func (m *MockStore) DeleteOldProvisionerDaemons(arg0 context.Context) error { +// DeleteOldNotificationReportGeneratorLogs mocks base method. +func (m *MockStore) DeleteOldNotificationReportGeneratorLogs(arg0 context.Context, arg1 database.DeleteOldNotificationReportGeneratorLogsParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOldProvisionerDaemons", arg0) + ret := m.ctrl.Call(m, "DeleteOldNotificationReportGeneratorLogs", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// DeleteOldProvisionerDaemons indicates an expected call of DeleteOldProvisionerDaemons. -func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 any) *gomock.Call { +// DeleteOldNotificationReportGeneratorLogs indicates an expected call of DeleteOldNotificationReportGeneratorLogs. +func (mr *MockStoreMockRecorder) DeleteOldNotificationReportGeneratorLogs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldProvisionerDaemons", reflect.TypeOf((*MockStore)(nil).DeleteOldProvisionerDaemons), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNotificationReportGeneratorLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldNotificationReportGeneratorLogs), arg0, arg1) } -// DeleteOldNotificationReportGeneratorLogs mocks base method. -func (m *MockStore) DeleteOldNotificationReportGeneratorLogs(arg0 context.Context, arg1 database.DeleteOldNotificationReportGeneratorLogsParams) error { +// DeleteOldProvisionerDaemons mocks base method. +func (m *MockStore) DeleteOldProvisionerDaemons(arg0 context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOldNotificationReportGeneratorLogs", arg0, arg1) + ret := m.ctrl.Call(m, "DeleteOldProvisionerDaemons", arg0) ret0, _ := ret[0].(error) return ret0 } -// DeleteOldNotificationReportGeneratorLogs indicates an expected call of DeleteOldNotificationReportGeneratorLogs. -func (mr *MockStoreMockRecorder) DeleteOldNotificationReportGeneratorLogs(arg0, arg1 any) *gomock.Call { +// DeleteOldProvisionerDaemons indicates an expected call of DeleteOldProvisionerDaemons. +func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNotificationReportGeneratorLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldNotificationReportGeneratorLogs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldProvisionerDaemons", reflect.TypeOf((*MockStore)(nil).DeleteOldProvisionerDaemons), arg0) } // DeleteOldWorkspaceAgentLogs mocks base method. @@ -1552,6 +1552,21 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationMessagesByStatus", reflect.TypeOf((*MockStore)(nil).GetNotificationMessagesByStatus), arg0, arg1) } +// GetNotificationReportGeneratorLogByUserAndTemplate mocks base method. +func (m *MockStore) GetNotificationReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByUserAndTemplate", arg0, arg1) + ret0, _ := ret[0].(database.ReportGeneratorLog) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNotificationReportGeneratorLogByUserAndTemplate indicates an expected call of GetNotificationReportGeneratorLogByUserAndTemplate. +func (mr *MockStoreMockRecorder) GetNotificationReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetNotificationReportGeneratorLogByUserAndTemplate), arg0, arg1) +} + // GetNotificationTemplateByID mocks base method. func (m *MockStore) GetNotificationTemplateByID(arg0 context.Context, arg1 uuid.UUID) (database.NotificationTemplate, error) { m.ctrl.T.Helper() @@ -2062,21 +2077,6 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1) } -// GetNotificationReportGeneratorLogByUserAndTemplate mocks base method. -func (m *MockStore) GetNotificationReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByUserAndTemplate", arg0, arg1) - ret0, _ := ret[0].(database.ReportGeneratorLog) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetNotificationReportGeneratorLogByUserAndTemplate indicates an expected call of GetNotificationReportGeneratorLogByUserAndTemplate. -func (mr *MockStoreMockRecorder) GetNotificationReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetNotificationReportGeneratorLogByUserAndTemplate), arg0, arg1) -} - // GetRuntimeConfig mocks base method. func (m *MockStore) GetRuntimeConfig(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() @@ -5226,6 +5226,20 @@ func (mr *MockStoreMockRecorder) UpsertLogoURL(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertLogoURL", reflect.TypeOf((*MockStore)(nil).UpsertLogoURL), arg0, arg1) } +// UpsertNotificationReportGeneratorLog mocks base method. +func (m *MockStore) UpsertNotificationReportGeneratorLog(arg0 context.Context, arg1 database.UpsertNotificationReportGeneratorLogParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertNotificationReportGeneratorLog", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertNotificationReportGeneratorLog indicates an expected call of UpsertNotificationReportGeneratorLog. +func (mr *MockStoreMockRecorder) UpsertNotificationReportGeneratorLog(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertNotificationReportGeneratorLog), arg0, arg1) +} + // UpsertNotificationsSettings mocks base method. func (m *MockStore) UpsertNotificationsSettings(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -5269,20 +5283,6 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) } -// UpsertNotificationReportGeneratorLog mocks base method. -func (m *MockStore) UpsertNotificationReportGeneratorLog(arg0 context.Context, arg1 database.UpsertNotificationReportGeneratorLogParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertNotificationReportGeneratorLog", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertNotificationReportGeneratorLog indicates an expected call of UpsertNotificationReportGeneratorLog. -func (mr *MockStoreMockRecorder) UpsertNotificationReportGeneratorLog(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationReportGeneratorLog", reflect.TypeOf((*MockStore)(nil).UpsertNotificationReportGeneratorLog), arg0, arg1) -} - // UpsertRuntimeConfig mocks base method. func (m *MockStore) UpsertRuntimeConfig(arg0 context.Context, arg1 database.UpsertRuntimeConfigParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 6a85e8546d5cf..64f1fd04ae30c 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -82,13 +82,13 @@ type sqlcQuerier interface { DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error // Delete all notification messages which have not been updated for over a week. DeleteOldNotificationMessages(ctx context.Context) error + // Delete report generator logs that have been created at least a @before date. + DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error // Delete provisioner daemons that have been created at least a week ago // and have not connected to coderd since a week. // A provisioner daemon with "zeroed" last_seen_at column indicates possible // connectivity issues (no provisioner daemon activity since registration). DeleteOldProvisionerDaemons(ctx context.Context) error - // Delete report generator logs that have been created at least a @before date. - DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error // If an agent hasn't connected in the last 7 days, we purge it's logs. // Exception: if the logs are related to the latest build, we keep those around. // Logs can take up a lot of space, so it's important we clean up frequently. @@ -169,6 +169,8 @@ type sqlcQuerier interface { GetLicenses(ctx context.Context) ([]License, error) GetLogoURL(ctx context.Context) (string, error) GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) + // Fetch the notification report generator log indicating recent activity. + GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) GetNotificationsSettings(ctx context.Context) (string, error) @@ -203,8 +205,6 @@ type sqlcQuerier interface { GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error) GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) - // Fetch the report generator log indicating recent activity. - GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) GetRuntimeConfig(ctx context.Context, key string) (string, error) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) @@ -487,11 +487,11 @@ type sqlcQuerier interface { UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error UpsertLastUpdateCheck(ctx context.Context, value string) error UpsertLogoURL(ctx context.Context, value string) error + // Insert or update notification report generator logs with recent activity. + UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error UpsertNotificationsSettings(ctx context.Context, value string) error UpsertOAuthSigningKey(ctx context.Context, value string) error UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) - // Insert or update report generator logs with recent activity. - UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f0fb52e4cac0f..aecf94a293e63 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3128,7 +3128,7 @@ func (q *sqlQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, } const upsertJFrogXrayScanByWorkspaceAndAgentID = `-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec -INSERT INTO +INSERT INTO jfrog_xray_scans ( agent_id, workspace_id, @@ -3137,7 +3137,7 @@ INSERT INTO medium, results_url ) -VALUES +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_id) DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6 @@ -3552,8 +3552,8 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { return err } -const DeleteOldNotificationReportGeneratorLogs = `-- name: DeleteOldNotificationReportGeneratorLogs :exec -DELETE FROM notification_report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 +const deleteOldNotificationReportGeneratorLogs = `-- name: DeleteOldNotificationReportGeneratorLogs :exec +DELETE FROM report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 ` type DeleteOldNotificationReportGeneratorLogsParams struct { @@ -3563,7 +3563,7 @@ type DeleteOldNotificationReportGeneratorLogsParams struct { // Delete report generator logs that have been created at least a @before date. func (q *sqlQuerier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error { - _, err := q.db.ExecContext(ctx, DeleteOldNotificationReportGeneratorLogs, arg.Before, arg.NotificationTemplateID) + _, err := q.db.ExecContext(ctx, deleteOldNotificationReportGeneratorLogs, arg.Before, arg.NotificationTemplateID) return err } @@ -3704,6 +3704,29 @@ func (q *sqlQuerier) GetNotificationMessagesByStatus(ctx context.Context, arg Ge return items, nil } +const getNotificationReportGeneratorLogByUserAndTemplate = `-- name: GetNotificationReportGeneratorLogByUserAndTemplate :one +SELECT + user_id, notification_template_id, last_generated_at +FROM + report_generator_logs +WHERE + user_id = $1 + AND notification_template_id = $2 +` + +type GetNotificationReportGeneratorLogByUserAndTemplateParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` +} + +// Fetch the notification report generator log indicating recent activity. +func (q *sqlQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) { + row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) + var i ReportGeneratorLog + err := row.Scan(&i.UserID, &i.NotificationTemplateID, &i.LastGeneratedAt) + return i, err +} + const getNotificationTemplateByID = `-- name: GetNotificationTemplateByID :one SELECT id, name, title_template, body_template, actions, "group", method, kind FROM notification_templates @@ -3765,29 +3788,6 @@ func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind No return items, nil } -const GetNotificationReportGeneratorLogByUserAndTemplate = `-- name: GetNotificationReportGeneratorLogByUserAndTemplate :one -SELECT - user_id, notification_template_id, last_generated_at -FROM - notification_report_generator_logs -WHERE - user_id = $1 - AND notification_template_id = $2 -` - -type GetNotificationReportGeneratorLogByUserAndTemplateParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` -} - -// Fetch the report generator log indicating recent activity. -func (q *sqlQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) { - row := q.db.QueryRowContext(ctx, GetNotificationReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) - var i ReportGeneratorLog - err := row.Scan(&i.UserID, &i.NotificationTemplateID, &i.LastGeneratedAt) - return i, err -} - const getUserNotificationPreferences = `-- name: GetUserNotificationPreferences :many SELECT user_id, notification_template_id, disabled, created_at, updated_at FROM notification_preferences @@ -3876,10 +3876,10 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg return result.RowsAffected() } -const UpsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec -INSERT INTO notification_report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) +const upsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec +INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE notification_report_generator_logs.user_id = EXCLUDED.user_id AND notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id +WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id ` type UpsertNotificationReportGeneratorLogParams struct { @@ -3888,9 +3888,9 @@ type UpsertNotificationReportGeneratorLogParams struct { LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` } -// Insert or update report generator logs with recent activity. +// Insert or update notification report generator logs with recent activity. func (q *sqlQuerier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error { - _, err := q.db.ExecContext(ctx, UpsertNotificationReportGeneratorLog, arg.UserID, arg.NotificationTemplateID, arg.LastGeneratedAt) + _, err := q.db.ExecContext(ctx, upsertNotificationReportGeneratorLog, arg.UserID, arg.NotificationTemplateID, arg.LastGeneratedAt) return err } @@ -5924,7 +5924,7 @@ FROM provisioner_keys WHERE organization_id = $1 -AND +AND lower(name) = lower($2) ` @@ -7677,7 +7677,7 @@ func (q *sqlQuerier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUI } const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :exec -UPDATE +UPDATE tailnet_peers SET status = $2 diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 836aaf461d4de..927bb15bfda32 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -46,7 +46,7 @@ const ( UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); - UniqueReportGeneratorLogsPkey UniqueConstraint = "notification_report_generator_logs_pkey" // ALTER TABLE ONLY notification_report_generator_logs ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); + UniqueReportGeneratorLogsPkey UniqueConstraint = "report_generator_logs_pkey" // ALTER TABLE ONLY report_generator_logs ADD CONSTRAINT report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id); diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts new file mode 100644 index 0000000000000..64bdb2d262852 --- /dev/null +++ b/site/src/api/typesGenerated.ts @@ -0,0 +1,2377 @@ +// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. + +// The code below is generated from codersdk. + +// From codersdk/templates.go +export interface ACLAvailable { + readonly users: Readonly>; + readonly groups: Readonly>; +} + +// From codersdk/apikey.go +export interface APIKey { + readonly id: string; + readonly user_id: string; + readonly last_used: string; + readonly expires_at: string; + readonly created_at: string; + readonly updated_at: string; + readonly login_type: LoginType; + readonly scope: APIKeyScope; + readonly token_name: string; + readonly lifetime_seconds: number; +} + +// From codersdk/apikey.go +export interface APIKeyWithOwner extends APIKey { + readonly username: string; +} + +// From codersdk/licenses.go +export interface AddLicenseRequest { + readonly license: string; +} + +// From codersdk/templates.go +export interface AgentStatsReportResponse { + readonly num_comms: number; + readonly rx_bytes: number; + readonly tx_bytes: number; +} + +// From codersdk/deployment.go +export interface AppHostResponse { + readonly host: string; +} + +// From codersdk/deployment.go +export interface AppearanceConfig { + readonly application_name: string; + readonly logo_url: string; + readonly service_banner: BannerConfig; + readonly announcement_banners: Readonly>; + readonly support_links?: Readonly>; +} + +// From codersdk/templates.go +export interface ArchiveTemplateVersionsRequest { + readonly all: boolean; +} + +// From codersdk/templates.go +export interface ArchiveTemplateVersionsResponse { + readonly template_id: string; + readonly archived_ids: Readonly>; +} + +// From codersdk/roles.go +export interface AssignableRoles extends Role { + readonly assignable: boolean; + readonly built_in: boolean; +} + +// From codersdk/audit.go +export type AuditDiff = Record + +// From codersdk/audit.go +export interface AuditDiffField { + // empty interface{} type, falling back to unknown + readonly old?: unknown; + // empty interface{} type, falling back to unknown + readonly new?: unknown; + readonly secret: boolean; +} + +// From codersdk/audit.go +export interface AuditLog { + readonly id: string; + readonly request_id: string; + readonly time: string; + readonly ip: string; + readonly user_agent: string; + readonly resource_type: ResourceType; + readonly resource_id: string; + readonly resource_target: string; + readonly resource_icon: string; + readonly action: AuditAction; + readonly diff: AuditDiff; + readonly status_code: number; + readonly additional_fields: Record; + readonly description: string; + readonly resource_link: string; + readonly is_deleted: boolean; + readonly organization_id: string; + readonly organization?: MinimalOrganization; + readonly user?: User; +} + +// From codersdk/audit.go +export interface AuditLogResponse { + readonly audit_logs: Readonly>; + readonly count: number; +} + +// From codersdk/audit.go +export interface AuditLogsRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/users.go +export interface AuthMethod { + readonly enabled: boolean; +} + +// From codersdk/users.go +export interface AuthMethods { + readonly terms_of_service_url?: string; + readonly password: AuthMethod; + readonly github: AuthMethod; + readonly oidc: OIDCAuthMethod; +} + +// From codersdk/authorization.go +export interface AuthorizationCheck { + readonly object: AuthorizationObject; + readonly action: RBACAction; +} + +// From codersdk/authorization.go +export interface AuthorizationObject { + readonly resource_type: RBACResource; + readonly owner_id?: string; + readonly organization_id?: string; + readonly resource_id?: string; + readonly any_org?: boolean; +} + +// From codersdk/authorization.go +export interface AuthorizationRequest { + readonly checks: Record; +} + +// From codersdk/authorization.go +export type AuthorizationResponse = Record + +// From codersdk/deployment.go +export interface AvailableExperiments { + readonly safe: Readonly>; +} + +// From codersdk/deployment.go +export interface BannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + +// From codersdk/deployment.go +export interface BuildInfoResponse { + readonly external_url: string; + readonly version: string; + readonly dashboard_url: string; + readonly telemetry: boolean; + readonly workspace_proxy: boolean; + readonly agent_api_version: string; + readonly provisioner_api_version: string; + readonly upgrade_message: string; + readonly deployment_id: string; +} + +// From codersdk/insights.go +export interface ConnectionLatency { + readonly p50: number; + readonly p95: number; +} + +// From codersdk/users.go +export interface ConvertLoginRequest { + readonly to_type: LoginType; + readonly password: string; +} + +// From codersdk/users.go +export interface CreateFirstUserRequest { + readonly email: string; + readonly username: string; + readonly name: string; + readonly password: string; + readonly trial: boolean; + readonly trial_info: CreateFirstUserTrialInfo; +} + +// From codersdk/users.go +export interface CreateFirstUserResponse { + readonly user_id: string; + readonly organization_id: string; +} + +// From codersdk/users.go +export interface CreateFirstUserTrialInfo { + readonly first_name: string; + readonly last_name: string; + readonly phone_number: string; + readonly job_title: string; + readonly company_name: string; + readonly country: string; + readonly developers: string; +} + +// From codersdk/groups.go +export interface CreateGroupRequest { + readonly name: string; + readonly display_name: string; + readonly avatar_url: string; + readonly quota_allowance: number; +} + +// From codersdk/organizations.go +export interface CreateOrganizationRequest { + readonly name: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; +} + +// From codersdk/provisionerdaemons.go +export interface CreateProvisionerKeyRequest { + readonly name: string; + readonly tags: Record; +} + +// From codersdk/provisionerdaemons.go +export interface CreateProvisionerKeyResponse { + readonly key: string; +} + +// From codersdk/organizations.go +export interface CreateTemplateRequest { + readonly name: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly template_version_id: string; + readonly default_ttl_ms?: number; + readonly activity_bump_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly failure_ttl_ms?: number; + readonly dormant_ttl_ms?: number; + readonly delete_ttl_ms?: number; + readonly disable_everyone_group_access: boolean; + readonly require_active_version: boolean; + readonly max_port_share_level?: WorkspaceAgentPortShareLevel; +} + +// From codersdk/templateversions.go +export interface CreateTemplateVersionDryRunRequest { + readonly workspace_name: string; + readonly rich_parameter_values: Readonly>; + readonly user_variable_values?: Readonly>; +} + +// From codersdk/organizations.go +export interface CreateTemplateVersionRequest { + readonly name?: string; + readonly message?: string; + readonly template_id?: string; + readonly storage_method: ProvisionerStorageMethod; + readonly file_id?: string; + readonly example_id?: string; + readonly provisioner: ProvisionerType; + readonly tags: Record; + readonly user_variable_values?: Readonly>; +} + +// From codersdk/audit.go +export interface CreateTestAuditLogRequest { + readonly action?: AuditAction; + readonly resource_type?: ResourceType; + readonly resource_id?: string; + readonly additional_fields?: Record; + readonly time?: string; + readonly build_reason?: BuildReason; + readonly organization_id?: string; +} + +// From codersdk/apikey.go +export interface CreateTokenRequest { + readonly lifetime: number; + readonly scope: APIKeyScope; + readonly token_name: string; +} + +// From codersdk/users.go +export interface CreateUserRequestWithOrgs { + readonly email: string; + readonly username: string; + readonly name: string; + readonly password: string; + readonly login_type: LoginType; + readonly organization_ids: Readonly>; +} + +// From codersdk/workspaces.go +export interface CreateWorkspaceBuildRequest { + readonly template_version_id?: string; + readonly transition: WorkspaceTransition; + readonly dry_run?: boolean; + readonly state?: string; + readonly orphan?: boolean; + readonly rich_parameter_values?: Readonly>; + readonly log_level?: ProvisionerLogLevel; +} + +// From codersdk/workspaceproxy.go +export interface CreateWorkspaceProxyRequest { + readonly name: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/organizations.go +export interface CreateWorkspaceRequest { + readonly template_id?: string; + readonly template_version_id?: string; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly rich_parameter_values?: Readonly>; + readonly automatic_updates?: AutomaticUpdates; +} + +// From codersdk/roles.go +export interface CustomRoleRequest { + readonly name: string; + readonly display_name: string; + readonly site_permissions: Readonly>; + readonly organization_permissions: Readonly>; + readonly user_permissions: Readonly>; +} + +// From codersdk/deployment.go +export interface DAUEntry { + readonly date: string; + readonly amount: number; +} + +// From codersdk/deployment.go +export interface DAURequest { + readonly TZHourOffset: number; +} + +// From codersdk/deployment.go +export interface DAUsResponse { + readonly entries: Readonly>; + readonly tz_hour_offset: number; +} + +// From codersdk/deployment.go +export interface DERP { + readonly server: DERPServerConfig; + readonly config: DERPConfig; +} + +// From codersdk/deployment.go +export interface DERPConfig { + readonly block_direct: boolean; + readonly force_websockets: boolean; + readonly url: string; + readonly path: string; +} + +// From codersdk/workspaceagents.go +export interface DERPRegion { + readonly preferred: boolean; + readonly latency_ms: number; +} + +// From codersdk/deployment.go +export interface DERPServerConfig { + readonly enable: boolean; + readonly region_id: number; + readonly region_code: string; + readonly region_name: string; + readonly stun_addresses: string[]; + readonly relay_url: string; +} + +// From codersdk/deployment.go +export interface DangerousConfig { + readonly allow_path_app_sharing: boolean; + readonly allow_path_app_site_owner_access: boolean; + readonly allow_all_cors: boolean; +} + +// From codersdk/workspaceagentportshare.go +export interface DeleteWorkspaceAgentPortShareRequest { + readonly agent_name: string; + readonly port: number; +} + +// From codersdk/deployment.go +export interface DeploymentConfig { + readonly config?: DeploymentValues; + readonly options?: SerpentOptionSet; +} + +// From codersdk/deployment.go +export interface DeploymentStats { + readonly aggregated_from: string; + readonly collected_at: string; + readonly next_update_at: string; + readonly workspaces: WorkspaceDeploymentStats; + readonly session_count: SessionCountDeploymentStats; +} + +// From codersdk/deployment.go +export interface DeploymentValues { + readonly verbose?: boolean; + readonly access_url?: string; + readonly wildcard_access_url?: string; + readonly docs_url?: string; + readonly redirect_to_access_url?: boolean; + readonly http_address?: string; + readonly autobuild_poll_interval?: number; + readonly job_hang_detector_interval?: number; + readonly derp?: DERP; + readonly prometheus?: PrometheusConfig; + readonly pprof?: PprofConfig; + readonly proxy_trusted_headers?: string[]; + readonly proxy_trusted_origins?: string[]; + readonly cache_directory?: string; + readonly in_memory_database?: boolean; + readonly pg_connection_url?: string; + readonly pg_auth?: string; + readonly oauth2?: OAuth2Config; + readonly oidc?: OIDCConfig; + readonly telemetry?: TelemetryConfig; + readonly tls?: TLSConfig; + readonly trace?: TraceConfig; + readonly secure_auth_cookie?: boolean; + readonly strict_transport_security?: number; + readonly strict_transport_security_options?: string[]; + readonly ssh_keygen_algorithm?: string; + readonly metrics_cache_refresh_interval?: number; + readonly agent_stat_refresh_interval?: number; + readonly agent_fallback_troubleshooting_url?: string; + readonly browser_only?: boolean; + readonly scim_api_key?: string; + readonly external_token_encryption_keys?: string[]; + readonly provisioner?: ProvisionerConfig; + readonly rate_limit?: RateLimitConfig; + readonly experiments?: string[]; + readonly update_check?: boolean; + readonly swagger?: SwaggerConfig; + readonly logging?: LoggingConfig; + readonly dangerous?: DangerousConfig; + readonly disable_path_apps?: boolean; + readonly session_lifetime?: SessionLifetime; + readonly disable_password_auth?: boolean; + readonly support?: SupportConfig; + readonly external_auth?: Readonly>; + readonly config_ssh?: SSHConfig; + readonly wgtunnel_host?: string; + readonly disable_owner_workspace_exec?: boolean; + readonly proxy_health_status_interval?: number; + readonly enable_terraform_debug_mode?: boolean; + readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig; + readonly web_terminal_renderer?: string; + readonly allow_workspace_renames?: boolean; + readonly healthcheck?: HealthcheckConfig; + readonly cli_upgrade_message?: string; + readonly terms_of_service_url?: string; + readonly notifications?: NotificationsConfig; + readonly config?: string; + readonly write_config?: boolean; + readonly address?: string; +} + +// From codersdk/deployment.go +export interface Entitlements { + readonly features: Record; + readonly warnings: Readonly>; + readonly errors: Readonly>; + readonly has_license: boolean; + readonly trial: boolean; + readonly require_telemetry: boolean; + readonly refreshed_at: string; +} + +// From codersdk/deployment.go +export type Experiments = Readonly> + +// From codersdk/externalauth.go +export interface ExternalAuth { + readonly authenticated: boolean; + readonly device: boolean; + readonly display_name: string; + readonly user?: ExternalAuthUser; + readonly app_installable: boolean; + readonly installations: Readonly>; + readonly app_install_url: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthAppInstallation { + readonly id: number; + readonly account: ExternalAuthUser; + readonly configure_url: string; +} + +// From codersdk/deployment.go +export interface ExternalAuthConfig { + readonly type: string; + readonly client_id: string; + readonly id: string; + readonly auth_url: string; + readonly token_url: string; + readonly validate_url: string; + readonly app_install_url: string; + readonly app_installations_url: string; + readonly no_refresh: boolean; + readonly scopes: Readonly>; + readonly device_flow: boolean; + readonly device_code_url: string; + readonly regex: string; + readonly display_name: string; + readonly display_icon: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthDevice { + readonly device_code: string; + readonly user_code: string; + readonly verification_uri: string; + readonly expires_in: number; + readonly interval: number; +} + +// From codersdk/externalauth.go +export interface ExternalAuthDeviceExchange { + readonly device_code: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLink { + readonly provider_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly has_refresh_token: boolean; + readonly expires: string; + readonly authenticated: boolean; + readonly validate_error: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLinkProvider { + readonly id: string; + readonly type: string; + readonly device: boolean; + readonly display_name: string; + readonly display_icon: string; + readonly allow_refresh: boolean; + readonly allow_validate: boolean; +} + +// From codersdk/externalauth.go +export interface ExternalAuthUser { + readonly id: number; + readonly login: string; + readonly avatar_url: string; + readonly profile_url: string; + readonly name: string; +} + +// From codersdk/deployment.go +export interface Feature { + readonly entitlement: Entitlement; + readonly enabled: boolean; + readonly limit?: number; + readonly actual?: number; +} + +// From codersdk/apikey.go +export interface GenerateAPIKeyResponse { + readonly key: string; +} + +// From codersdk/users.go +export interface GetUsersResponse { + readonly users: Readonly>; + readonly count: number; +} + +// From codersdk/gitsshkey.go +export interface GitSSHKey { + readonly user_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly public_key: string; +} + +// From codersdk/groups.go +export interface Group { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly organization_id: string; + readonly members: Readonly>; + readonly total_member_count: number; + readonly avatar_url: string; + readonly quota_allowance: number; + readonly source: GroupSource; + readonly organization_name: string; + readonly organization_display_name: string; +} + +// From codersdk/groups.go +export interface GroupArguments { + readonly Organization: string; + readonly HasMember: string; +} + +// From codersdk/workspaceapps.go +export interface Healthcheck { + readonly url: string; + readonly interval: number; + readonly threshold: number; +} + +// From codersdk/deployment.go +export interface HealthcheckConfig { + readonly refresh: number; + readonly threshold_database: number; +} + +// From codersdk/workspaceagents.go +export interface IssueReconnectingPTYSignedTokenRequest { + readonly url: string; + readonly agentID: string; +} + +// From codersdk/workspaceagents.go +export interface IssueReconnectingPTYSignedTokenResponse { + readonly signed_token: string; +} + +// From codersdk/jfrog.go +export interface JFrogXrayScan { + readonly workspace_id: string; + readonly agent_id: string; + readonly critical: number; + readonly high: number; + readonly medium: number; + readonly results_url: string; +} + +// From codersdk/licenses.go +export interface License { + readonly id: number; + readonly uuid: string; + readonly uploaded_at: string; + // empty interface{} type, falling back to unknown + readonly claims: Record; +} + +// From codersdk/deployment.go +export interface LinkConfig { + readonly name: string; + readonly target: string; + readonly icon: string; +} + +// From codersdk/externalauth.go +export interface ListUserExternalAuthResponse { + readonly providers: Readonly>; + readonly links: Readonly>; +} + +// From codersdk/deployment.go +export interface LoggingConfig { + readonly log_filter: string[]; + readonly human: string; + readonly json: string; + readonly stackdriver: string; +} + +// From codersdk/users.go +export interface LoginWithPasswordRequest { + readonly email: string; + readonly password: string; +} + +// From codersdk/users.go +export interface LoginWithPasswordResponse { + readonly session_token: string; +} + +// From codersdk/organizations.go +export interface MinimalOrganization { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/users.go +export interface MinimalUser { + readonly id: string; + readonly username: string; + readonly avatar_url: string; +} + +// From codersdk/notifications.go +export interface NotificationMethodsResponse { + readonly available: Readonly>; + readonly default: string; +} + +// From codersdk/notifications.go +export interface NotificationPreference { + readonly id: string; + readonly disabled: boolean; + readonly updated_at: string; +} + +// From codersdk/notifications.go +export interface NotificationTemplate { + readonly id: string; + readonly name: string; + readonly title_template: string; + readonly body_template: string; + readonly actions: string; + readonly group: string; + readonly method: string; + readonly kind: string; +} + +// From codersdk/deployment.go +export interface NotificationsConfig { + readonly max_send_attempts: number; + readonly retry_interval: number; + readonly sync_interval: number; + readonly sync_buffer_size: number; + readonly lease_period: number; + readonly lease_count: number; + readonly fetch_interval: number; + readonly method: string; + readonly dispatch_timeout: number; + readonly email: NotificationsEmailConfig; + readonly webhook: NotificationsWebhookConfig; +} + +// From codersdk/deployment.go +export interface NotificationsEmailAuthConfig { + readonly identity: string; + readonly username: string; + readonly password: string; + readonly password_file: string; +} + +// From codersdk/deployment.go +export interface NotificationsEmailConfig { + readonly from: string; + readonly smarthost: string; + readonly hello: string; + readonly auth: NotificationsEmailAuthConfig; + readonly tls: NotificationsEmailTLSConfig; + readonly force_tls: boolean; +} + +// From codersdk/deployment.go +export interface NotificationsEmailTLSConfig { + readonly start_tls: boolean; + readonly server_name: string; + readonly insecure_skip_verify: boolean; + readonly ca_file: string; + readonly cert_file: string; + readonly key_file: string; +} + +// From codersdk/notifications.go +export interface NotificationsSettings { + readonly notifier_paused: boolean; +} + +// From codersdk/deployment.go +export interface NotificationsWebhookConfig { + readonly endpoint: string; +} + +// From codersdk/oauth2.go +export interface OAuth2AppEndpoints { + readonly authorization: string; + readonly token: string; + readonly device_authorization: string; +} + +// From codersdk/deployment.go +export interface OAuth2Config { + readonly github: OAuth2GithubConfig; +} + +// From codersdk/deployment.go +export interface OAuth2GithubConfig { + readonly client_id: string; + readonly client_secret: string; + readonly allowed_orgs: string[]; + readonly allowed_teams: string[]; + readonly allow_signups: boolean; + readonly allow_everyone: boolean; + readonly enterprise_base_url: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderApp { + readonly id: string; + readonly name: string; + readonly callback_url: string; + readonly icon: string; + readonly endpoints: OAuth2AppEndpoints; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppFilter { + readonly user_id?: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppSecret { + readonly id: string; + readonly last_used_at?: string; + readonly client_secret_truncated: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppSecretFull { + readonly id: string; + readonly client_secret_full: string; +} + +// From codersdk/users.go +export interface OAuthConversionResponse { + readonly state_string: string; + readonly expires_at: string; + readonly to_type: LoginType; + readonly user_id: string; +} + +// From codersdk/users.go +export interface OIDCAuthMethod extends AuthMethod { + readonly signInText: string; + readonly iconUrl: string; +} + +// From codersdk/deployment.go +export interface OIDCConfig { + readonly allow_signups: boolean; + readonly client_id: string; + readonly client_secret: string; + readonly client_key_file: string; + readonly client_cert_file: string; + readonly email_domain: string[]; + readonly issuer_url: string; + readonly scopes: string[]; + readonly ignore_email_verified: boolean; + readonly username_field: string; + readonly name_field: string; + readonly email_field: string; + readonly auth_url_params: Record; + readonly ignore_user_info: boolean; + readonly organization_field: string; + readonly organization_mapping: Record>>; + readonly organization_assign_default: boolean; + readonly group_auto_create: boolean; + readonly group_regex_filter: string; + readonly group_allow_list: string[]; + readonly groups_field: string; + readonly group_mapping: Record; + readonly user_role_field: string; + readonly user_role_mapping: Record>>; + readonly user_roles_default: string[]; + readonly sign_in_text: string; + readonly icon_url: string; + readonly signups_disabled_text: string; + readonly skip_issuer_checks: boolean; +} + +// From codersdk/organizations.go +export interface Organization extends MinimalOrganization { + readonly description: string; + readonly created_at: string; + readonly updated_at: string; + readonly is_default: boolean; +} + +// From codersdk/organizations.go +export interface OrganizationMember { + readonly user_id: string; + readonly organization_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly roles: Readonly>; +} + +// From codersdk/organizations.go +export interface OrganizationMemberWithUserData extends OrganizationMember { + readonly username: string; + readonly name: string; + readonly avatar_url: string; + readonly email: string; + readonly global_roles: Readonly>; +} + +// From codersdk/pagination.go +export interface Pagination { + readonly after_id?: string; + readonly limit?: number; + readonly offset?: number; +} + +// From codersdk/groups.go +export interface PatchGroupRequest { + readonly add_users: Readonly>; + readonly remove_users: Readonly>; + readonly name: string; + readonly display_name?: string; + readonly avatar_url?: string; + readonly quota_allowance?: number; +} + +// From codersdk/templateversions.go +export interface PatchTemplateVersionRequest { + readonly name: string; + readonly message?: string; +} + +// From codersdk/workspaceproxy.go +export interface PatchWorkspaceProxy { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon: string; + readonly regenerate_token: boolean; +} + +// From codersdk/roles.go +export interface Permission { + readonly negate: boolean; + readonly resource_type: RBACResource; + readonly action: RBACAction; +} + +// From codersdk/oauth2.go +export interface PostOAuth2ProviderAppRequest { + readonly name: string; + readonly callback_url: string; + readonly icon: string; +} + +// From codersdk/workspaces.go +export interface PostWorkspaceUsageRequest { + readonly agent_id: string; + readonly app_name: UsageAppName; +} + +// From codersdk/deployment.go +export interface PprofConfig { + readonly enable: boolean; + readonly address: string; +} + +// From codersdk/deployment.go +export interface PrometheusConfig { + readonly enable: boolean; + readonly address: string; + readonly collect_agent_stats: boolean; + readonly collect_db_metrics: boolean; + readonly aggregate_agent_stats_by: string[]; +} + +// From codersdk/deployment.go +export interface ProvisionerConfig { + readonly daemons: number; + readonly daemon_types: string[]; + readonly daemon_poll_interval: number; + readonly daemon_poll_jitter: number; + readonly force_cancel_interval: number; + readonly daemon_psk: string; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemon { + readonly id: string; + readonly organization_id: string; + readonly created_at: string; + readonly last_seen_at?: string; + readonly name: string; + readonly version: string; + readonly api_version: string; + readonly provisioners: Readonly>; + readonly tags: Record; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJob { + readonly id: string; + readonly created_at: string; + readonly started_at?: string; + readonly completed_at?: string; + readonly canceled_at?: string; + readonly error?: string; + readonly error_code?: JobErrorCode; + readonly status: ProvisionerJobStatus; + readonly worker_id?: string; + readonly file_id: string; + readonly tags: Record; + readonly queue_position: number; + readonly queue_size: number; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJobLog { + readonly id: number; + readonly created_at: string; + readonly log_source: LogSource; + readonly log_level: LogLevel; + readonly stage: string; + readonly output: string; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerKey { + readonly id: string; + readonly created_at: string; + readonly organization: string; + readonly name: string; + readonly tags: Record; +} + +// From codersdk/workspaceproxy.go +export interface ProxyHealthReport { + readonly errors: Readonly>; + readonly warnings: Readonly>; +} + +// From codersdk/workspaces.go +export interface PutExtendWorkspaceRequest { + readonly deadline: string; +} + +// From codersdk/oauth2.go +export interface PutOAuth2ProviderAppRequest { + readonly name: string; + readonly callback_url: string; + readonly icon: string; +} + +// From codersdk/deployment.go +export interface RateLimitConfig { + readonly disable_all: boolean; + readonly api: number; +} + +// From codersdk/users.go +export interface ReducedUser extends MinimalUser { + readonly name: string; + readonly email: string; + readonly created_at: string; + readonly updated_at: string; + readonly last_seen_at: string; + readonly status: UserStatus; + readonly login_type: LoginType; + readonly theme_preference: string; +} + +// From codersdk/workspaceproxy.go +export interface Region { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon_url: string; + readonly healthy: boolean; + readonly path_app_url: string; + readonly wildcard_hostname: string; +} + +// From codersdk/workspaceproxy.go +export interface RegionsResponse { + readonly regions: Readonly>; +} + +// From codersdk/replicas.go +export interface Replica { + readonly id: string; + readonly hostname: string; + readonly created_at: string; + readonly relay_address: string; + readonly region_id: number; + readonly error: string; + readonly database_latency: number; +} + +// From codersdk/workspaces.go +export interface ResolveAutostartResponse { + readonly parameter_mismatch: boolean; +} + +// From codersdk/client.go +export interface Response { + readonly message: string; + readonly detail?: string; + readonly validations?: Readonly>; +} + +// From codersdk/roles.go +export interface Role { + readonly name: string; + readonly organization_id?: string; + readonly display_name: string; + readonly site_permissions: Readonly>; + readonly organization_permissions: Readonly>; + readonly user_permissions: Readonly>; +} + +// From codersdk/deployment.go +export interface SSHConfig { + readonly DeploymentName: string; + readonly SSHConfigOptions: string[]; +} + +// From codersdk/deployment.go +export interface SSHConfigResponse { + readonly hostname_prefix: string; + readonly ssh_config_options: Record; +} + +// From codersdk/serversentevents.go +export interface ServerSentEvent { + readonly type: ServerSentEventType; + // empty interface{} type, falling back to unknown + readonly data: unknown; +} + +// From codersdk/deployment.go +export interface ServiceBannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + +// From codersdk/deployment.go +export interface SessionCountDeploymentStats { + readonly vscode: number; + readonly ssh: number; + readonly jetbrains: number; + readonly reconnecting_pty: number; +} + +// From codersdk/deployment.go +export interface SessionLifetime { + readonly disable_expiry_refresh?: boolean; + readonly default_duration: number; + readonly max_token_lifetime?: number; +} + +// From codersdk/roles.go +export interface SlimRole { + readonly name: string; + readonly display_name: string; + readonly organization_id?: string; +} + +// From codersdk/deployment.go +export interface SupportConfig { + readonly links: Readonly>; +} + +// From codersdk/deployment.go +export interface SwaggerConfig { + readonly enable: boolean; +} + +// From codersdk/deployment.go +export interface TLSConfig { + readonly enable: boolean; + readonly address: string; + readonly redirect_http: boolean; + readonly cert_file: string[]; + readonly client_auth: string; + readonly client_ca_file: string; + readonly key_file: string[]; + readonly min_version: string; + readonly client_cert_file: string; + readonly client_key_file: string; + readonly supported_ciphers: string[]; + readonly allow_insecure_ciphers: boolean; +} + +// From codersdk/deployment.go +export interface TelemetryConfig { + readonly enable: boolean; + readonly trace: boolean; + readonly url: string; +} + +// From codersdk/templates.go +export interface Template { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly organization_id: string; + readonly organization_name: string; + readonly organization_display_name: string; + readonly organization_icon: string; + readonly name: string; + readonly display_name: string; + readonly provisioner: ProvisionerType; + readonly active_version_id: string; + readonly active_user_count: number; + readonly build_time_stats: TemplateBuildTimeStats; + readonly description: string; + readonly deprecated: boolean; + readonly deprecation_message: string; + readonly icon: string; + readonly default_ttl_ms: number; + readonly activity_bump_ms: number; + readonly autostop_requirement: TemplateAutostopRequirement; + readonly autostart_requirement: TemplateAutostartRequirement; + readonly created_by_id: string; + readonly created_by_name: string; + readonly allow_user_autostart: boolean; + readonly allow_user_autostop: boolean; + readonly allow_user_cancel_workspace_jobs: boolean; + readonly failure_ttl_ms: number; + readonly time_til_dormant_ms: number; + readonly time_til_dormant_autodelete_ms: number; + readonly require_active_version: boolean; + readonly max_port_share_level: WorkspaceAgentPortShareLevel; +} + +// From codersdk/templates.go +export interface TemplateACL { + readonly users: Readonly>; + readonly group: Readonly>; +} + +// From codersdk/insights.go +export interface TemplateAppUsage { + readonly template_ids: Readonly>; + readonly type: TemplateAppsType; + readonly display_name: string; + readonly slug: string; + readonly icon: string; + readonly seconds: number; + readonly times_used: number; +} + +// From codersdk/templates.go +export interface TemplateAutostartRequirement { + readonly days_of_week: Readonly>; +} + +// From codersdk/templates.go +export interface TemplateAutostopRequirement { + readonly days_of_week: Readonly>; + readonly weeks: number; +} + +// From codersdk/templates.go +export type TemplateBuildTimeStats = Record + +// From codersdk/templates.go +export interface TemplateExample { + readonly id: string; + readonly url: string; + readonly name: string; + readonly description: string; + readonly icon: string; + readonly tags: Readonly>; + readonly markdown: string; +} + +// From codersdk/organizations.go +export interface TemplateFilter { + readonly q?: string; +} + +// From codersdk/templates.go +export interface TemplateGroup extends Group { + readonly role: TemplateRole; +} + +// From codersdk/insights.go +export interface TemplateInsightsIntervalReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; + readonly interval: InsightsReportInterval; + readonly active_users: number; +} + +// From codersdk/insights.go +export interface TemplateInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; + readonly active_users: number; + readonly apps_usage: Readonly>; + readonly parameters_usage: Readonly>; +} + +// From codersdk/insights.go +export interface TemplateInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; + readonly interval: InsightsReportInterval; + readonly sections: Readonly>; +} + +// From codersdk/insights.go +export interface TemplateInsightsResponse { + readonly report?: TemplateInsightsReport; + readonly interval_reports?: Readonly>; +} + +// From codersdk/insights.go +export interface TemplateParameterUsage { + readonly template_ids: Readonly>; + readonly display_name: string; + readonly name: string; + readonly type: string; + readonly description: string; + readonly options?: Readonly>; + readonly values: Readonly>; +} + +// From codersdk/insights.go +export interface TemplateParameterValue { + readonly value: string; + readonly count: number; +} + +// From codersdk/templates.go +export interface TemplateUser extends User { + readonly role: TemplateRole; +} + +// From codersdk/templateversions.go +export interface TemplateVersion { + readonly id: string; + readonly template_id?: string; + readonly organization_id?: string; + readonly created_at: string; + readonly updated_at: string; + readonly name: string; + readonly message: string; + readonly job: ProvisionerJob; + readonly readme: string; + readonly created_by: MinimalUser; + readonly archived: boolean; + readonly warnings?: Readonly>; +} + +// From codersdk/templateversions.go +export interface TemplateVersionExternalAuth { + readonly id: string; + readonly type: string; + readonly display_name: string; + readonly display_icon: string; + readonly authenticate_url: string; + readonly authenticated: boolean; + readonly optional?: boolean; +} + +// From codersdk/templateversions.go +export interface TemplateVersionParameter { + readonly name: string; + readonly display_name?: string; + readonly description: string; + readonly description_plaintext: string; + readonly type: string; + readonly mutable: boolean; + readonly default_value: string; + readonly icon: string; + readonly options: Readonly>; + readonly validation_error?: string; + readonly validation_regex?: string; + readonly validation_min?: number; + readonly validation_max?: number; + readonly validation_monotonic?: ValidationMonotonicOrder; + readonly required: boolean; + readonly ephemeral: boolean; +} + +// From codersdk/templateversions.go +export interface TemplateVersionParameterOption { + readonly name: string; + readonly description: string; + readonly value: string; + readonly icon: string; +} + +// From codersdk/templateversions.go +export interface TemplateVersionVariable { + readonly name: string; + readonly description: string; + readonly type: string; + readonly value: string; + readonly default_value: string; + readonly required: boolean; + readonly sensitive: boolean; +} + +// From codersdk/templates.go +export interface TemplateVersionsByTemplateRequest extends Pagination { + readonly template_id: string; + readonly include_archived: boolean; +} + +// From codersdk/apikey.go +export interface TokenConfig { + readonly max_token_lifetime: number; +} + +// From codersdk/apikey.go +export interface TokensFilter { + readonly include_all: boolean; +} + +// From codersdk/deployment.go +export interface TraceConfig { + readonly enable: boolean; + readonly honeycomb_api_key: string; + readonly capture_logs: boolean; + readonly data_dog: boolean; +} + +// From codersdk/templates.go +export interface TransitionStats { + readonly P50?: number; + readonly P95?: number; +} + +// From codersdk/templates.go +export interface UpdateActiveTemplateVersion { + readonly id: string; +} + +// From codersdk/deployment.go +export interface UpdateAppearanceConfig { + readonly application_name: string; + readonly logo_url: string; + readonly service_banner: BannerConfig; + readonly announcement_banners: Readonly>; +} + +// From codersdk/updatecheck.go +export interface UpdateCheckResponse { + readonly current: boolean; + readonly version: string; + readonly url: string; +} + +// From codersdk/notifications.go +export interface UpdateNotificationTemplateMethod { + readonly method?: string; +} + +// From codersdk/organizations.go +export interface UpdateOrganizationRequest { + readonly name?: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; +} + +// From codersdk/users.go +export interface UpdateRoles { + readonly roles: Readonly>; +} + +// From codersdk/templates.go +export interface UpdateTemplateACL { + readonly user_perms?: Record; + readonly group_perms?: Record; +} + +// From codersdk/templates.go +export interface UpdateTemplateMeta { + readonly name?: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly default_ttl_ms?: number; + readonly activity_bump_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly failure_ttl_ms?: number; + readonly time_til_dormant_ms?: number; + readonly time_til_dormant_autodelete_ms?: number; + readonly update_workspace_last_used_at: boolean; + readonly update_workspace_dormant_at: boolean; + readonly require_active_version?: boolean; + readonly deprecation_message?: string; + readonly disable_everyone_group_access: boolean; + readonly max_port_share_level?: WorkspaceAgentPortShareLevel; +} + +// From codersdk/users.go +export interface UpdateUserAppearanceSettingsRequest { + readonly theme_preference: string; +} + +// From codersdk/notifications.go +export interface UpdateUserNotificationPreferences { + readonly template_disabled_map: Record; +} + +// From codersdk/users.go +export interface UpdateUserPasswordRequest { + readonly old_password: string; + readonly password: string; +} + +// From codersdk/users.go +export interface UpdateUserProfileRequest { + readonly username: string; + readonly name: string; +} + +// From codersdk/users.go +export interface UpdateUserQuietHoursScheduleRequest { + readonly schedule: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceAutomaticUpdatesRequest { + readonly automatic_updates: AutomaticUpdates; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceAutostartRequest { + readonly schedule?: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceDormancy { + readonly dormant: boolean; +} + +// From codersdk/workspaceproxy.go +export interface UpdateWorkspaceProxyResponse { + readonly proxy: WorkspaceProxy; + readonly proxy_token: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceRequest { + readonly name?: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceTTLRequest { + readonly ttl_ms?: number; +} + +// From codersdk/files.go +export interface UploadResponse { + readonly hash: string; +} + +// From codersdk/workspaceagentportshare.go +export interface UpsertWorkspaceAgentPortShareRequest { + readonly agent_name: string; + readonly port: number; + readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; +} + +// From codersdk/users.go +export interface User extends ReducedUser { + readonly organization_ids: Readonly>; + readonly roles: Readonly>; +} + +// From codersdk/insights.go +export interface UserActivity { + readonly template_ids: Readonly>; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly seconds: number; +} + +// From codersdk/insights.go +export interface UserActivityInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; + readonly users: Readonly>; +} + +// From codersdk/insights.go +export interface UserActivityInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; +} + +// From codersdk/insights.go +export interface UserActivityInsightsResponse { + readonly report: UserActivityInsightsReport; +} + +// From codersdk/insights.go +export interface UserLatency { + readonly template_ids: Readonly>; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly latency_ms: ConnectionLatency; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; + readonly users: Readonly>; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: Readonly>; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsResponse { + readonly report: UserLatencyInsightsReport; +} + +// From codersdk/users.go +export interface UserLoginType { + readonly login_type: LoginType; +} + +// From codersdk/users.go +export interface UserParameter { + readonly name: string; + readonly value: string; +} + +// From codersdk/deployment.go +export interface UserQuietHoursScheduleConfig { + readonly default_schedule: string; + readonly allow_user_custom: boolean; +} + +// From codersdk/users.go +export interface UserQuietHoursScheduleResponse { + readonly raw_schedule: string; + readonly user_set: boolean; + readonly user_can_set: boolean; + readonly time: string; + readonly timezone: string; + readonly next: string; +} + +// From codersdk/users.go +export interface UserRoles { + readonly roles: Readonly>; + readonly organization_roles: Record>>; +} + +// From codersdk/users.go +export interface UsersRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/client.go +export interface ValidationError { + readonly field: string; + readonly detail: string; +} + +// From codersdk/organizations.go +export interface VariableValue { + readonly name: string; + readonly value: string; +} + +// From codersdk/workspaces.go +export interface Workspace { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly owner_id: string; + readonly owner_name: string; + readonly owner_avatar_url: string; + readonly organization_id: string; + readonly organization_name: string; + readonly template_id: string; + readonly template_name: string; + readonly template_display_name: string; + readonly template_icon: string; + readonly template_allow_user_cancel_workspace_jobs: boolean; + readonly template_active_version_id: string; + readonly template_require_active_version: boolean; + readonly latest_build: WorkspaceBuild; + readonly outdated: boolean; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly last_used_at: string; + readonly deleting_at?: string; + readonly dormant_at?: string; + readonly health: WorkspaceHealth; + readonly automatic_updates: AutomaticUpdates; + readonly allow_renames: boolean; + readonly favorite: boolean; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgent { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly first_connected_at?: string; + readonly last_connected_at?: string; + readonly disconnected_at?: string; + readonly started_at?: string; + readonly ready_at?: string; + readonly status: WorkspaceAgentStatus; + readonly lifecycle_state: WorkspaceAgentLifecycle; + readonly name: string; + readonly resource_id: string; + readonly instance_id?: string; + readonly architecture: string; + readonly environment_variables: Record; + readonly operating_system: string; + readonly logs_length: number; + readonly logs_overflowed: boolean; + readonly directory?: string; + readonly expanded_directory?: string; + readonly version: string; + readonly api_version: string; + readonly apps: Readonly>; + readonly latency?: Record; + readonly connection_timeout_seconds: number; + readonly troubleshooting_url: string; + readonly subsystems: Readonly>; + readonly health: WorkspaceAgentHealth; + readonly display_apps: Readonly>; + readonly log_sources: Readonly>; + readonly scripts: Readonly>; + readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentHealth { + readonly healthy: boolean; + readonly reason?: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListeningPort { + readonly process_name: string; + readonly network: string; + readonly port: number; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListeningPortsResponse { + readonly ports: Readonly>; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentLog { + readonly id: number; + readonly created_at: string; + readonly output: string; + readonly level: LogLevel; + readonly source_id: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentLogSource { + readonly workspace_agent_id: string; + readonly id: string; + readonly created_at: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadata { + readonly result: WorkspaceAgentMetadataResult; + readonly description: WorkspaceAgentMetadataDescription; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataDescription { + readonly display_name: string; + readonly key: string; + readonly script: string; + readonly interval: number; + readonly timeout: number; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataResult { + readonly collected_at: string; + readonly age: number; + readonly value: string; + readonly error: string; +} + +// From codersdk/workspaceagentportshare.go +export interface WorkspaceAgentPortShare { + readonly workspace_id: string; + readonly agent_name: string; + readonly port: number; + readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; +} + +// From codersdk/workspaceagentportshare.go +export interface WorkspaceAgentPortShares { + readonly shares: Readonly>; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentScript { + readonly log_source_id: string; + readonly log_path: string; + readonly script: string; + readonly cron: string; + readonly run_on_start: boolean; + readonly run_on_stop: boolean; + readonly start_blocks_login: boolean; + readonly timeout: number; +} + +// From codersdk/workspaceapps.go +export interface WorkspaceApp { + readonly id: string; + readonly url: string; + readonly external: boolean; + readonly slug: string; + readonly display_name: string; + readonly command?: string; + readonly icon?: string; + readonly subdomain: boolean; + readonly subdomain_name?: string; + readonly sharing_level: WorkspaceAppSharingLevel; + readonly healthcheck: Healthcheck; + readonly health: WorkspaceAppHealth; + readonly hidden: boolean; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceBuild { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly workspace_id: string; + readonly workspace_name: string; + readonly workspace_owner_id: string; + readonly workspace_owner_name: string; + readonly workspace_owner_avatar_url: string; + readonly template_version_id: string; + readonly template_version_name: string; + readonly build_number: number; + readonly transition: WorkspaceTransition; + readonly initiator_id: string; + readonly initiator_name: string; + readonly job: ProvisionerJob; + readonly reason: BuildReason; + readonly resources: Readonly>; + readonly deadline?: string; + readonly max_deadline?: string; + readonly status: WorkspaceStatus; + readonly daily_cost: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceBuildParameter { + readonly name: string; + readonly value: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceBuildsRequest extends Pagination { + readonly since?: string; +} + +// From codersdk/deployment.go +export interface WorkspaceConnectionLatencyMS { + readonly P50: number; + readonly P95: number; +} + +// From codersdk/deployment.go +export interface WorkspaceDeploymentStats { + readonly pending: number; + readonly building: number; + readonly running: number; + readonly failed: number; + readonly stopped: number; + readonly connection_latency_ms: WorkspaceConnectionLatencyMS; + readonly rx_bytes: number; + readonly tx_bytes: number; +} + +// From codersdk/workspaces.go +export interface WorkspaceFilter { + readonly q?: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceHealth { + readonly healthy: boolean; + readonly failing_agents: Readonly>; +} + +// From codersdk/workspaces.go +export interface WorkspaceOptions { + readonly include_deleted?: boolean; +} + +// From codersdk/workspaceproxy.go +export interface WorkspaceProxy extends Region { + readonly derp_enabled: boolean; + readonly derp_only: boolean; + readonly status?: WorkspaceProxyStatus; + readonly created_at: string; + readonly updated_at: string; + readonly deleted: boolean; + readonly version: string; +} + +// From codersdk/deployment.go +export interface WorkspaceProxyBuildInfo { + readonly workspace_proxy: boolean; + readonly dashboard_url: string; +} + +// From codersdk/workspaceproxy.go +export interface WorkspaceProxyStatus { + readonly status: ProxyHealthStatus; + readonly report?: ProxyHealthReport; + readonly checked_at: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceQuota { + readonly credits_consumed: number; + readonly budget: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceResource { + readonly id: string; + readonly created_at: string; + readonly job_id: string; + readonly workspace_transition: WorkspaceTransition; + readonly type: string; + readonly name: string; + readonly hide: boolean; + readonly icon: string; + readonly agents?: Readonly>; + readonly metadata?: Readonly>; + readonly daily_cost: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceResourceMetadata { + readonly key: string; + readonly value: string; + readonly sensitive: boolean; +} + +// From codersdk/workspaces.go +export interface WorkspacesRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/workspaces.go +export interface WorkspacesResponse { + readonly workspaces: Readonly>; + readonly count: number; +} + +// From codersdk/apikey.go +export type APIKeyScope = "all" | "application_connect" +export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] + +// From codersdk/workspaceagents.go +export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace" +export const AgentSubsystems: AgentSubsystem[] = ["envbox", "envbuilder", "exectrace"] + +// From codersdk/audit.go +export type AuditAction = "create" | "delete" | "login" | "logout" | "register" | "start" | "stop" | "write" +export const AuditActions: AuditAction[] = ["create", "delete", "login", "logout", "register", "start", "stop", "write"] + +// From codersdk/workspaces.go +export type AutomaticUpdates = "always" | "never" +export const AutomaticUpdateses: AutomaticUpdates[] = ["always", "never"] + +// From codersdk/workspacebuilds.go +export type BuildReason = "autostart" | "autostop" | "initiator" +export const BuildReasons: BuildReason[] = ["autostart", "autostop", "initiator"] + +// From codersdk/workspaceagents.go +export type DisplayApp = "port_forwarding_helper" | "ssh_helper" | "vscode" | "vscode_insiders" | "web_terminal" +export const DisplayApps: DisplayApp[] = ["port_forwarding_helper", "ssh_helper", "vscode", "vscode_insiders", "web_terminal"] + +// From codersdk/externalauth.go +export type EnhancedExternalAuthProvider = "azure-devops" | "azure-devops-entra" | "bitbucket-cloud" | "bitbucket-server" | "gitea" | "github" | "gitlab" | "jfrog" | "slack" +export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = ["azure-devops", "azure-devops-entra", "bitbucket-cloud", "bitbucket-server", "gitea", "github", "gitlab", "jfrog", "slack"] + +// From codersdk/deployment.go +export type Entitlement = "entitled" | "grace_period" | "not_entitled" +export const Entitlements: Entitlement[] = ["entitled", "grace_period", "not_entitled"] + +// From codersdk/deployment.go +export type Experiment = "auto-fill-parameters" | "custom-roles" | "example" | "multi-organization" | "notifications" | "workspace-usage" +export const Experiments: Experiment[] = ["auto-fill-parameters", "custom-roles", "example", "multi-organization", "notifications", "workspace-usage"] + +// From codersdk/deployment.go +export type FeatureName = "access_control" | "advanced_template_scheduling" | "appearance" | "audit_log" | "browser_only" | "control_shared_ports" | "custom_roles" | "external_provisioner_daemons" | "external_token_encryption" | "high_availability" | "multiple_external_auth" | "multiple_organizations" | "scim" | "template_rbac" | "user_limit" | "user_role_management" | "workspace_batch_actions" | "workspace_proxy" +export const FeatureNames: FeatureName[] = ["access_control", "advanced_template_scheduling", "appearance", "audit_log", "browser_only", "control_shared_ports", "custom_roles", "external_provisioner_daemons", "external_token_encryption", "high_availability", "multiple_external_auth", "multiple_organizations", "scim", "template_rbac", "user_limit", "user_role_management", "workspace_batch_actions", "workspace_proxy"] + +// From codersdk/deployment.go +export type FeatureSet = "" | "enterprise" | "premium" +export const FeatureSets: FeatureSet[] = ["", "enterprise", "premium"] + +// From codersdk/groups.go +export type GroupSource = "oidc" | "user" +export const GroupSources: GroupSource[] = ["oidc", "user"] + +// From codersdk/insights.go +export type InsightsReportInterval = "day" | "week" +export const InsightsReportIntervals: InsightsReportInterval[] = ["day", "week"] + +// From codersdk/provisionerdaemons.go +export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES" +export const JobErrorCodes: JobErrorCode[] = ["REQUIRED_TEMPLATE_VARIABLES"] + +// From codersdk/provisionerdaemons.go +export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" +export const LogLevels: LogLevel[] = ["debug", "error", "info", "trace", "warn"] + +// From codersdk/provisionerdaemons.go +export type LogSource = "provisioner" | "provisioner_daemon" +export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"] + +// From codersdk/apikey.go +export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token" +export const LoginTypes: LoginType[] = ["", "github", "none", "oidc", "password", "token"] + +// From codersdk/oauth2.go +export type OAuth2ProviderGrantType = "authorization_code" | "refresh_token" +export const OAuth2ProviderGrantTypes: OAuth2ProviderGrantType[] = ["authorization_code", "refresh_token"] + +// From codersdk/oauth2.go +export type OAuth2ProviderResponseType = "code" +export const OAuth2ProviderResponseTypes: OAuth2ProviderResponseType[] = ["code"] + +// From codersdk/deployment.go +export type PostgresAuth = "awsiamrds" | "password" +export const PostgresAuths: PostgresAuth[] = ["awsiamrds", "password"] + +// From codersdk/provisionerdaemons.go +export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded" | "unknown" +export const ProvisionerJobStatuses: ProvisionerJobStatus[] = ["canceled", "canceling", "failed", "pending", "running", "succeeded", "unknown"] + +// From codersdk/workspaces.go +export type ProvisionerLogLevel = "debug" +export const ProvisionerLogLevels: ProvisionerLogLevel[] = ["debug"] + +// From codersdk/organizations.go +export type ProvisionerStorageMethod = "file" +export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ["file"] + +// From codersdk/organizations.go +export type ProvisionerType = "echo" | "terraform" +export const ProvisionerTypes: ProvisionerType[] = ["echo", "terraform"] + +// From codersdk/workspaceproxy.go +export type ProxyHealthStatus = "ok" | "unhealthy" | "unreachable" | "unregistered" +export const ProxyHealthStatuses: ProxyHealthStatus[] = ["ok", "unhealthy", "unreachable", "unregistered"] + +// From codersdk/rbacresources_gen.go +export type RBACAction = "application_connect" | "assign" | "create" | "delete" | "read" | "read_personal" | "ssh" | "start" | "stop" | "update" | "update_personal" | "use" | "view_insights" +export const RBACActions: RBACAction[] = ["application_connect", "assign", "create", "delete", "read", "read_personal", "ssh", "start", "stop", "update", "update_personal", "use", "view_insights"] + +// From codersdk/rbacresources_gen.go +export type RBACResource = "*" | "api_key" | "assign_org_role" | "assign_role" | "audit_log" | "debug_info" | "deployment_config" | "deployment_stats" | "file" | "group" | "group_member" | "license" | "notification_preference" | "notification_template" | "oauth2_app" | "oauth2_app_code_token" | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" | "template" | "user" | "workspace" | "workspace_dormant" | "workspace_proxy" +export const RBACResources: RBACResource[] = ["*", "api_key", "assign_org_role", "assign_role", "audit_log", "debug_info", "deployment_config", "deployment_stats", "file", "group", "group_member", "license", "notification_preference", "notification_template", "oauth2_app", "oauth2_app_code_token", "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", "workspace_dormant", "workspace_proxy"] + +// From codersdk/audit.go +export type ResourceType = "api_key" | "convert_login" | "custom_role" | "git_ssh_key" | "group" | "health_settings" | "license" | "notifications_settings" | "oauth2_provider_app" | "oauth2_provider_app_secret" | "organization" | "template" | "template_version" | "user" | "workspace" | "workspace_build" | "workspace_proxy" +export const ResourceTypes: ResourceType[] = ["api_key", "convert_login", "custom_role", "git_ssh_key", "group", "health_settings", "license", "notifications_settings", "oauth2_provider_app", "oauth2_provider_app_secret", "organization", "template", "template_version", "user", "workspace", "workspace_build", "workspace_proxy"] + +// From codersdk/serversentevents.go +export type ServerSentEventType = "data" | "error" | "ping" +export const ServerSentEventTypes: ServerSentEventType[] = ["data", "error", "ping"] + +// From codersdk/insights.go +export type TemplateAppsType = "app" | "builtin" +export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"] + +// From codersdk/insights.go +export type TemplateInsightsSection = "interval_reports" | "report" +export const TemplateInsightsSections: TemplateInsightsSection[] = ["interval_reports", "report"] + +// From codersdk/templates.go +export type TemplateRole = "" | "admin" | "use" +export const TemplateRoles: TemplateRole[] = ["", "admin", "use"] + +// From codersdk/templateversions.go +export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES" +export const TemplateVersionWarnings: TemplateVersionWarning[] = ["UNSUPPORTED_WORKSPACES"] + +// From codersdk/workspaces.go +export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode" +export const UsageAppNames: UsageAppName[] = ["jetbrains", "reconnecting-pty", "ssh", "vscode"] + +// From codersdk/users.go +export type UserStatus = "active" | "dormant" | "suspended" +export const UserStatuses: UserStatus[] = ["active", "dormant", "suspended"] + +// From codersdk/templateversions.go +export type ValidationMonotonicOrder = "decreasing" | "increasing" +export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = ["decreasing", "increasing"] + +// From codersdk/workspaceagents.go +export type WorkspaceAgentLifecycle = "created" | "off" | "ready" | "shutdown_error" | "shutdown_timeout" | "shutting_down" | "start_error" | "start_timeout" | "starting" +export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = ["created", "off", "ready", "shutdown_error", "shutdown_timeout", "shutting_down", "start_error", "start_timeout", "starting"] + +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareLevel = "authenticated" | "owner" | "public" +export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = ["authenticated", "owner", "public"] + +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareProtocol = "http" | "https" +export const WorkspaceAgentPortShareProtocols: WorkspaceAgentPortShareProtocol[] = ["http", "https"] + +// From codersdk/workspaceagents.go +export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking" +export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] = ["blocking", "non-blocking"] + +// From codersdk/workspaceagents.go +export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" | "timeout" +export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = ["connected", "connecting", "disconnected", "timeout"] + +// From codersdk/workspaceapps.go +export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy" +export const WorkspaceAppHealths: WorkspaceAppHealth[] = ["disabled", "healthy", "initializing", "unhealthy"] + +// From codersdk/workspaceapps.go +export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" +export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = ["authenticated", "owner", "public"] + +// From codersdk/workspacebuilds.go +export type WorkspaceStatus = "canceled" | "canceling" | "deleted" | "deleting" | "failed" | "pending" | "running" | "starting" | "stopped" | "stopping" +export const WorkspaceStatuses: WorkspaceStatus[] = ["canceled", "canceling", "deleted", "deleting", "failed", "pending", "running", "starting", "stopped", "stopping"] + +// From codersdk/workspacebuilds.go +export type WorkspaceTransition = "delete" | "start" | "stop" +export const WorkspaceTransitions: WorkspaceTransition[] = ["delete", "start", "stop"] + +// From codersdk/workspaceproxy.go +export type RegionTypes = Region | WorkspaceProxy + +// The code below is generated from codersdk/healthsdk. + +// From healthsdk/healthsdk.go +export interface AccessURLReport extends BaseReport { + readonly healthy: boolean; + readonly access_url: string; + readonly reachable: boolean; + readonly status_code: number; + readonly healthz_response: string; +} + +// From healthsdk/healthsdk.go +export interface BaseReport { + readonly error?: string; + readonly severity: HealthSeverity; + readonly warnings: Readonly>; + readonly dismissed: boolean; +} + +// From healthsdk/healthsdk.go +export interface DERPHealthReport extends BaseReport { + readonly healthy: boolean; + readonly regions: Record; + // TODO: narrow this type + readonly netcheck?: any; + readonly netcheck_err?: string; + readonly netcheck_logs: Readonly>; +} + +// From healthsdk/healthsdk.go +export interface DERPNodeReport { + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly warnings: Readonly>; + readonly error?: string; + // TODO: narrow this type + readonly node?: any; + // TODO: narrow this type + readonly node_info: any; + readonly can_exchange_messages: boolean; + readonly round_trip_ping: string; + readonly round_trip_ping_ms: number; + readonly uses_websocket: boolean; + readonly client_logs: Readonly>>>; + readonly client_errs: Readonly>>>; + readonly stun: STUNReport; +} + +// From healthsdk/healthsdk.go +export interface DERPRegionReport { + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly warnings: Readonly>; + readonly error?: string; + // TODO: narrow this type + readonly region?: any; + readonly node_reports: Readonly>; +} + +// From healthsdk/healthsdk.go +export interface DatabaseReport extends BaseReport { + readonly healthy: boolean; + readonly reachable: boolean; + readonly latency: string; + readonly latency_ms: number; + readonly threshold_ms: number; +} + +// From healthsdk/healthsdk.go +export interface HealthSettings { + readonly dismissed_healthchecks: Readonly>; +} + +// From healthsdk/healthsdk.go +export interface HealthcheckReport { + readonly time: string; + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly derp: DERPHealthReport; + readonly access_url: AccessURLReport; + readonly websocket: WebsocketReport; + readonly database: DatabaseReport; + readonly workspace_proxy: WorkspaceProxyReport; + readonly provisioner_daemons: ProvisionerDaemonsReport; + readonly coder_version: string; +} + +// From healthsdk/healthsdk.go +export interface ProvisionerDaemonsReport extends BaseReport { + readonly items: Readonly>; +} + +// From healthsdk/healthsdk.go +export interface ProvisionerDaemonsReportItem { + readonly provisioner_daemon: ProvisionerDaemon; + readonly warnings: Readonly>; +} + +// From healthsdk/healthsdk.go +export interface STUNReport { + readonly Enabled: boolean; + readonly CanSTUN: boolean; + readonly Error?: string; +} + +// From healthsdk/healthsdk.go +export interface UpdateHealthSettings { + readonly dismissed_healthchecks: Readonly>; +} + +// From healthsdk/healthsdk.go +export interface WebsocketReport extends BaseReport { + readonly healthy: boolean; + readonly body: string; + readonly code: number; +} + +// From healthsdk/healthsdk.go +export interface WorkspaceProxyReport extends BaseReport { + readonly healthy: boolean; + readonly workspace_proxies: RegionsResponse; +} + +// From healthsdk/healthsdk.go +export type HealthSection = "AccessURL" | "DERP" | "Database" | "ProvisionerDaemons" | "Websocket" | "WorkspaceProxy" +export const HealthSections: HealthSection[] = ["AccessURL", "DERP", "Database", "ProvisionerDaemons", "Websocket", "WorkspaceProxy"] + +// The code below is generated from coderd/healthcheck/health. + +// From health/model.go +export interface HealthMessage { + readonly code: HealthCode; + readonly message: string; +} + +// From health/model.go +export type HealthCode = "EACS01" | "EACS02" | "EACS03" | "EACS04" | "EDB01" | "EDB02" | "EDERP01" | "EDERP02" | "EPD01" | "EPD02" | "EPD03" | "EUNKNOWN" | "EWP01" | "EWP02" | "EWP04" | "EWS01" | "EWS02" | "EWS03" +export const HealthCodes: HealthCode[] = ["EACS01", "EACS02", "EACS03", "EACS04", "EDB01", "EDB02", "EDERP01", "EDERP02", "EPD01", "EPD02", "EPD03", "EUNKNOWN", "EWP01", "EWP02", "EWP04", "EWS01", "EWS02", "EWS03"] + +// From health/model.go +export type HealthSeverity = "error" | "ok" | "warning" +export const HealthSeveritys: HealthSeverity[] = ["error", "ok", "warning"] + +// The code below is generated from github.com/coder/serpent. + +// From serpent/serpent.go +export type SerpentAnnotations = Record + +// From serpent/serpent.go +export interface SerpentGroup { + readonly parent?: SerpentGroup; + readonly name?: string; + readonly yaml?: string; + readonly description?: string; +} + +// From serpent/option.go +export interface SerpentOption { + readonly name?: string; + readonly description?: string; + readonly required?: boolean; + readonly flag?: string; + readonly flag_shorthand?: string; + readonly env?: string; + readonly yaml?: string; + readonly default?: string; + // TODO: narrow this type + readonly value?: any; + readonly annotations?: SerpentAnnotations; + readonly group?: SerpentGroup; + readonly use_instead?: Readonly>; + readonly hidden?: boolean; + readonly value_source?: SerpentValueSource; +} + +// From serpent/option.go +export type SerpentOptionSet = Readonly> + +// From serpent/option.go +export type SerpentValueSource = "" | "default" | "env" | "flag" | "yaml" +export const SerpentValueSources: SerpentValueSource[] = ["", "default", "env", "flag", "yaml"] + From 4cacf4dd89e42760926d6f02490b516924851c35 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 10:53:58 +0200 Subject: [PATCH 100/122] WIP --- coderd/database/migrations/000250_email_reports.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000250_email_reports.up.sql b/coderd/database/migrations/000250_email_reports.up.sql index f4c8b2f4df19f..b4f99d20cd45a 100644 --- a/coderd/database/migrations/000250_email_reports.up.sql +++ b/coderd/database/migrations/000250_email_reports.up.sql @@ -19,7 +19,7 @@ We recommend reviewing these issues to ensure future builds are successful.', } ]'::jsonb); -CREATE TABLE report_generator_logs +CREATE TABLE notification_report_generator_logs ( user_id uuid NOT NULL, notification_template_id uuid NOT NULL, @@ -28,4 +28,4 @@ CREATE TABLE report_generator_logs PRIMARY KEY (user_id, notification_template_id) ); -COMMENT ON TABLE report_generator_logs IS 'Log of generated reports for users.'; +COMMENT ON TABLE notification_report_generator_logs IS 'Log of generated reports for users.'; From f23e8374d0f2ca99950e8f87720536b4037f9f54 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 10:54:57 +0200 Subject: [PATCH 101/122] WIP --- coderd/database/queries/notifications.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 3a65ad49adf85..287ed9959f878 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -180,17 +180,17 @@ ORDER BY name ASC; SELECT * FROM - report_generator_logs + notification_report_generator_logs WHERE user_id = $1 AND notification_template_id = $2; -- name: UpsertNotificationReportGeneratorLog :exec -- Insert or update notification report generator logs with recent activity. -INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) +INSERT INTO notification_report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; -- name: DeleteOldNotificationReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a @before date. -DELETE FROM report_generator_logs WHERE last_generated_at < @before::timestamptz AND notification_template_id = @notification_template_id; +DELETE FROM notification_report_generator_logs WHERE last_generated_at < @before::timestamptz AND notification_template_id = @notification_template_id; From 359416cf8509d3410d541fc80961f67bfc07f99c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 08:59:32 +0000 Subject: [PATCH 102/122] notifications --- coderd/database/dbauthz/dbauthz.go | 4 ++-- coderd/database/dbmem/dbmem.go | 22 +++++++++++----------- coderd/database/dbmetrics/dbmetrics.go | 2 +- coderd/database/dbmock/dbmock.go | 4 ++-- coderd/database/dump.sql | 22 +++++++++++----------- coderd/database/models.go | 14 +++++++------- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 10 +++++----- coderd/database/unique_constraint.go | 2 +- 9 files changed, 41 insertions(+), 41 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 20d81c8bbb5d4..d04def0457266 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1613,9 +1613,9 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab return q.db.GetNotificationMessagesByStatus(ctx, arg) } -func (q *querier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (q *querier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { - return database.ReportGeneratorLog{}, err + return database.NotificationReportGeneratorLog{}, err } return q.db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8c8617841417a..8cd4af2dfd6aa 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -156,7 +156,7 @@ type data struct { dbcryptKeys []database.DBCryptKey files []database.File externalAuthLinks []database.ExternalAuthLink - reportGeneratorLogs []database.ReportGeneratorLog + notificationReportGeneratorLogs []database.NotificationReportGeneratorLog gitSSHKey []database.GitSSHKey groupMembers []database.GroupMemberTable groups []database.Group @@ -1694,13 +1694,13 @@ func (q *FakeQuerier) DeleteOldNotificationReportGeneratorLogs(_ context.Context q.mutex.Lock() defer q.mutex.Unlock() - var validLogs []database.ReportGeneratorLog - for _, record := range q.reportGeneratorLogs { + var validLogs []database.NotificationReportGeneratorLog + for _, record := range q.notificationReportGeneratorLogs { if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.After(params.Before) { validLogs = append(validLogs, record) } } - q.reportGeneratorLogs = validLogs + q.notificationReportGeneratorLogs = validLogs return nil } @@ -3002,21 +3002,21 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat return out, nil } -func (q *FakeQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (q *FakeQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { err := validateDatabaseType(arg) if err != nil { - return database.ReportGeneratorLog{}, err + return database.NotificationReportGeneratorLog{}, err } q.mutex.RLock() defer q.mutex.RUnlock() - for _, record := range q.reportGeneratorLogs { + for _, record := range q.notificationReportGeneratorLogs { if record.UserID == arg.UserID && record.NotificationTemplateID == arg.NotificationTemplateID { return record, nil } } - return database.ReportGeneratorLog{}, sql.ErrNoRows + return database.NotificationReportGeneratorLog{}, sql.ErrNoRows } func (*FakeQuerier) GetNotificationTemplateByID(_ context.Context, _ uuid.UUID) (database.NotificationTemplate, error) { @@ -9383,14 +9383,14 @@ func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, ar q.mutex.Lock() defer q.mutex.Unlock() - for i, record := range q.reportGeneratorLogs { + for i, record := range q.notificationReportGeneratorLogs { if arg.NotificationTemplateID == record.NotificationTemplateID && arg.UserID == record.UserID { - q.reportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt + q.notificationReportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt return nil } } - q.reportGeneratorLogs = append(q.reportGeneratorLogs, database.ReportGeneratorLog(arg)) + q.notificationReportGeneratorLogs = append(q.notificationReportGeneratorLogs, database.NotificationReportGeneratorLog(arg)) return nil } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 55ff9f01fde26..6850be1673344 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -774,7 +774,7 @@ func (m metricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg d return r0, r1 } -func (m metricsStore) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (m metricsStore) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { start := time.Now() r0, r1 := m.s.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) m.queryLatencies.WithLabelValues("GetNotificationReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index da6a0a866c5fb..51541bf625af2 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1553,10 +1553,10 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any) } // GetNotificationReportGeneratorLogByUserAndTemplate mocks base method. -func (m *MockStore) GetNotificationReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.ReportGeneratorLog, error) { +func (m *MockStore) GetNotificationReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByUserAndTemplate", arg0, arg1) - ret0, _ := ret[0].(database.ReportGeneratorLog) + ret0, _ := ret[0].(database.NotificationReportGeneratorLog) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 2fb25faadc051..5d367ef14a249 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -717,6 +717,14 @@ CREATE TABLE notification_preferences ( updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL ); +CREATE TABLE notification_report_generator_logs ( + user_id uuid NOT NULL, + notification_template_id uuid NOT NULL, + last_generated_at timestamp with time zone NOT NULL +); + +COMMENT ON TABLE notification_report_generator_logs IS 'Log of generated reports for users.'; + CREATE TABLE notification_templates ( id uuid NOT NULL, name text NOT NULL, @@ -946,14 +954,6 @@ CREATE TABLE replicas ( "primary" boolean DEFAULT true NOT NULL ); -CREATE TABLE report_generator_logs ( - user_id uuid NOT NULL, - notification_template_id uuid NOT NULL, - last_generated_at timestamp with time zone NOT NULL -); - -COMMENT ON TABLE report_generator_logs IS 'Log of generated reports for users.'; - CREATE TABLE site_configs ( key character varying(256) NOT NULL, value text NOT NULL @@ -1696,6 +1696,9 @@ ALTER TABLE ONLY notification_messages ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id); +ALTER TABLE ONLY notification_report_generator_logs + ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); + ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name); @@ -1759,9 +1762,6 @@ ALTER TABLE ONLY provisioner_jobs ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); -ALTER TABLE ONLY report_generator_logs - ADD CONSTRAINT report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); - ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); diff --git a/coderd/database/models.go b/coderd/database/models.go index 96cc3e18e41cb..021dc37088811 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2192,6 +2192,13 @@ type NotificationPreference struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +// Log of generated reports for users. +type NotificationReportGeneratorLog struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` + LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` +} + // Templates from which to create notification messages. type NotificationTemplate struct { ID uuid.UUID `db:"id" json:"id"` @@ -2397,13 +2404,6 @@ type Replica struct { Primary bool `db:"primary" json:"primary"` } -// Log of generated reports for users. -type ReportGeneratorLog struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` - LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` -} - type SiteConfig struct { Key string `db:"key" json:"key"` Value string `db:"value" json:"value"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 64f1fd04ae30c..116d667a43194 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -170,7 +170,7 @@ type sqlcQuerier interface { GetLogoURL(ctx context.Context) (string, error) GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) // Fetch the notification report generator log indicating recent activity. - GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) + GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (NotificationReportGeneratorLog, error) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) GetNotificationsSettings(ctx context.Context) (string, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index aecf94a293e63..75567ae901cf3 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3553,7 +3553,7 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { } const deleteOldNotificationReportGeneratorLogs = `-- name: DeleteOldNotificationReportGeneratorLogs :exec -DELETE FROM report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 +DELETE FROM notification_report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 ` type DeleteOldNotificationReportGeneratorLogsParams struct { @@ -3708,7 +3708,7 @@ const getNotificationReportGeneratorLogByUserAndTemplate = `-- name: GetNotifica SELECT user_id, notification_template_id, last_generated_at FROM - report_generator_logs + notification_report_generator_logs WHERE user_id = $1 AND notification_template_id = $2 @@ -3720,9 +3720,9 @@ type GetNotificationReportGeneratorLogByUserAndTemplateParams struct { } // Fetch the notification report generator log indicating recent activity. -func (q *sqlQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (ReportGeneratorLog, error) { +func (q *sqlQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (NotificationReportGeneratorLog, error) { row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) - var i ReportGeneratorLog + var i NotificationReportGeneratorLog err := row.Scan(&i.UserID, &i.NotificationTemplateID, &i.LastGeneratedAt) return i, err } @@ -3877,7 +3877,7 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg } const upsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec -INSERT INTO report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) +INSERT INTO notification_report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id ` diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 927bb15bfda32..86bfa82d82932 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -25,6 +25,7 @@ const ( UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id); UniqueNotificationMessagesPkey UniqueConstraint = "notification_messages_pkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id); UniqueNotificationPreferencesPkey UniqueConstraint = "notification_preferences_pkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id); + UniqueNotificationReportGeneratorLogsPkey UniqueConstraint = "notification_report_generator_logs_pkey" // ALTER TABLE ONLY notification_report_generator_logs ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); UniqueNotificationTemplatesNameKey UniqueConstraint = "notification_templates_name_key" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name); UniqueNotificationTemplatesPkey UniqueConstraint = "notification_templates_pkey" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_pkey PRIMARY KEY (id); UniqueOauth2ProviderAppCodesPkey UniqueConstraint = "oauth2_provider_app_codes_pkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_pkey PRIMARY KEY (id); @@ -46,7 +47,6 @@ const ( UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); - UniqueReportGeneratorLogsPkey UniqueConstraint = "report_generator_logs_pkey" // ALTER TABLE ONLY report_generator_logs ADD CONSTRAINT report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id); From 68ed60f86723350f5a8670fbd2e7ada473f85274 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 11:00:58 +0200 Subject: [PATCH 103/122] fmt --- coderd/database/dbmem/dbmem.go | 92 +++++++++++++++++----------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8cd4af2dfd6aa..502049abb5f6f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -151,52 +151,52 @@ type data struct { userLinks []database.UserLink // New tables - workspaceAgentStats []database.WorkspaceAgentStat - auditLogs []database.AuditLog - dbcryptKeys []database.DBCryptKey - files []database.File - externalAuthLinks []database.ExternalAuthLink - notificationReportGeneratorLogs []database.NotificationReportGeneratorLog - gitSSHKey []database.GitSSHKey - groupMembers []database.GroupMemberTable - groups []database.Group - jfrogXRayScans []database.JfrogXrayScan - licenses []database.License - notificationMessages []database.NotificationMessage - notificationPreferences []database.NotificationPreference - oauth2ProviderApps []database.OAuth2ProviderApp - oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret - oauth2ProviderAppCodes []database.OAuth2ProviderAppCode - oauth2ProviderAppTokens []database.OAuth2ProviderAppToken - parameterSchemas []database.ParameterSchema - provisionerDaemons []database.ProvisionerDaemon - provisionerJobLogs []database.ProvisionerJobLog - provisionerJobs []database.ProvisionerJob - provisionerKeys []database.ProvisionerKey - replicas []database.Replica - templateVersions []database.TemplateVersionTable - templateVersionParameters []database.TemplateVersionParameter - templateVersionVariables []database.TemplateVersionVariable - templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag - templates []database.TemplateTable - templateUsageStats []database.TemplateUsageStat - workspaceAgents []database.WorkspaceAgent - workspaceAgentMetadata []database.WorkspaceAgentMetadatum - workspaceAgentLogs []database.WorkspaceAgentLog - workspaceAgentLogSources []database.WorkspaceAgentLogSource - workspaceAgentScripts []database.WorkspaceAgentScript - workspaceAgentPortShares []database.WorkspaceAgentPortShare - workspaceApps []database.WorkspaceApp - workspaceAppStatsLastInsertID int64 - workspaceAppStats []database.WorkspaceAppStat - workspaceBuilds []database.WorkspaceBuild - workspaceBuildParameters []database.WorkspaceBuildParameter - workspaceResourceMetadata []database.WorkspaceResourceMetadatum - workspaceResources []database.WorkspaceResource - workspaces []database.Workspace - workspaceProxies []database.WorkspaceProxy - customRoles []database.CustomRole - runtimeConfig map[string]string + workspaceAgentStats []database.WorkspaceAgentStat + auditLogs []database.AuditLog + dbcryptKeys []database.DBCryptKey + files []database.File + externalAuthLinks []database.ExternalAuthLink + notificationReportGeneratorLogs []database.NotificationReportGeneratorLog + gitSSHKey []database.GitSSHKey + groupMembers []database.GroupMemberTable + groups []database.Group + jfrogXRayScans []database.JfrogXrayScan + licenses []database.License + notificationMessages []database.NotificationMessage + notificationPreferences []database.NotificationPreference + oauth2ProviderApps []database.OAuth2ProviderApp + oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret + oauth2ProviderAppCodes []database.OAuth2ProviderAppCode + oauth2ProviderAppTokens []database.OAuth2ProviderAppToken + parameterSchemas []database.ParameterSchema + provisionerDaemons []database.ProvisionerDaemon + provisionerJobLogs []database.ProvisionerJobLog + provisionerJobs []database.ProvisionerJob + provisionerKeys []database.ProvisionerKey + replicas []database.Replica + templateVersions []database.TemplateVersionTable + templateVersionParameters []database.TemplateVersionParameter + templateVersionVariables []database.TemplateVersionVariable + templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag + templates []database.TemplateTable + templateUsageStats []database.TemplateUsageStat + workspaceAgents []database.WorkspaceAgent + workspaceAgentMetadata []database.WorkspaceAgentMetadatum + workspaceAgentLogs []database.WorkspaceAgentLog + workspaceAgentLogSources []database.WorkspaceAgentLogSource + workspaceAgentScripts []database.WorkspaceAgentScript + workspaceAgentPortShares []database.WorkspaceAgentPortShare + workspaceApps []database.WorkspaceApp + workspaceAppStatsLastInsertID int64 + workspaceAppStats []database.WorkspaceAppStat + workspaceBuilds []database.WorkspaceBuild + workspaceBuildParameters []database.WorkspaceBuildParameter + workspaceResourceMetadata []database.WorkspaceResourceMetadatum + workspaceResources []database.WorkspaceResource + workspaces []database.Workspace + workspaceProxies []database.WorkspaceProxy + customRoles []database.CustomRole + runtimeConfig map[string]string // Locks is a map of lock names. Any keys within the map are currently // locked. locks map[int64]struct{} From fdad7453e6f17da3590cef358e7084e639908ed2 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 11:02:21 +0200 Subject: [PATCH 104/122] rephrase --- cli/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/server.go b/cli/server.go index 08d72820414aa..dbe00bdc015e7 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1024,8 +1024,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. notificationsManager.Run(dbauthz.AsSystemRestricted(ctx)) // Run report generator to distribute periodic reports. - reportGenerator := reports.NewReportGenerator(ctx, logger, options.Database, options.NotificationsEnqueuer, quartz.NewReal()) - defer reportGenerator.Close() + notificationReportGenerator := reports.NewReportGenerator(ctx, logger, options.Database, options.NotificationsEnqueuer, quartz.NewReal()) + defer notificationReportGenerator.Close() } // Wrap the server in middleware that redirects to the access URL if From 9ba4c04a73a283ec7abec470427dc7596d4066f5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 11:48:50 +0200 Subject: [PATCH 105/122] WIP --- coderd/database/dbauthz/dbauthz.go | 4 +-- coderd/database/dbauthz/dbauthz_test.go | 8 ++--- coderd/database/dbmem/dbmem.go | 4 +-- coderd/database/dbmetrics/dbmetrics.go | 6 ++-- coderd/database/dbmock/dbmock.go | 12 ++++---- coderd/database/dump.sql | 3 +- .../migrations/000250_email_reports.up.sql | 3 +- coderd/database/models.go | 3 +- coderd/database/querier.go | 4 +-- coderd/database/queries.sql.go | 29 +++++++------------ coderd/database/queries/notifications.sql | 11 ++++--- coderd/database/unique_constraint.go | 2 +- coderd/notifications/reports/generator.go | 2 +- 13 files changed, 38 insertions(+), 53 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index d04def0457266..908045a367863 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1613,11 +1613,11 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab return q.db.GetNotificationMessagesByStatus(ctx, arg) } -func (q *querier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { +func (q *querier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.NotificationReportGeneratorLog{}, err } - return q.db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) + return q.db.GetNotificationReportGeneratorLogByTemplate(ctx, arg) } func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0efef575a4427..1419f8ed296d9 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2747,15 +2747,12 @@ func (s *MethodTestSuite) TestSystemFunctions() { Since: dbtime.Now(), }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetNotificationReportGeneratorLogByUserAndTemplate", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) + s.Run("GetNotificationReportGeneratorLogByTemplate", s.Subtest(func(db database.Store, check *expects) { _ = db.UpsertNotificationReportGeneratorLog(context.Background(), database.UpsertNotificationReportGeneratorLogParams{ - UserID: u.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, LastGeneratedAt: dbtime.Now(), }) - check.Args(database.GetNotificationReportGeneratorLogByUserAndTemplateParams{ - UserID: u.ID, + check.Args(database.GetNotificationReportGeneratorLogByTemplateParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }).Asserts(rbac.ResourceSystem, policy.ActionRead) })) @@ -2764,7 +2761,6 @@ func (s *MethodTestSuite) TestSystemFunctions() { })) s.Run("UpsertNotificationReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) { check.Args(database.UpsertNotificationReportGeneratorLogParams{ - UserID: uuid.New(), NotificationTemplateID: uuid.New(), LastGeneratedAt: dbtime.Now(), }).Asserts(rbac.ResourceSystem, policy.ActionCreate) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 502049abb5f6f..8d0e73bdf8d04 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3002,7 +3002,7 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat return out, nil } -func (q *FakeQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(_ context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { +func (q *FakeQuerier) GetNotificationReportGeneratorLogByTemplate(_ context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) { err := validateDatabaseType(arg) if err != nil { return database.NotificationReportGeneratorLog{}, err @@ -9384,7 +9384,7 @@ func (q *FakeQuerier) UpsertNotificationReportGeneratorLog(_ context.Context, ar defer q.mutex.Unlock() for i, record := range q.notificationReportGeneratorLogs { - if arg.NotificationTemplateID == record.NotificationTemplateID && arg.UserID == record.UserID { + if arg.NotificationTemplateID == record.NotificationTemplateID { q.notificationReportGeneratorLogs[i].LastGeneratedAt = arg.LastGeneratedAt return nil } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 6850be1673344..cdae7bc96478a 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -774,10 +774,10 @@ func (m metricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg d return r0, r1 } -func (m metricsStore) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { +func (m metricsStore) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) { start := time.Now() - r0, r1 := m.s.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, arg) - m.queryLatencies.WithLabelValues("GetNotificationReportGeneratorLogByUserAndTemplate").Observe(time.Since(start).Seconds()) + r0, r1 := m.s.GetNotificationReportGeneratorLogByTemplate(ctx, arg) + m.queryLatencies.WithLabelValues("GetNotificationReportGeneratorLogByTemplate").Observe(time.Since(start).Seconds()) return r0, r1 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 51541bf625af2..450b6d209cc96 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1552,19 +1552,19 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationMessagesByStatus", reflect.TypeOf((*MockStore)(nil).GetNotificationMessagesByStatus), arg0, arg1) } -// GetNotificationReportGeneratorLogByUserAndTemplate mocks base method. -func (m *MockStore) GetNotificationReportGeneratorLogByUserAndTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByUserAndTemplateParams) (database.NotificationReportGeneratorLog, error) { +// GetNotificationReportGeneratorLogByTemplate mocks base method. +func (m *MockStore) GetNotificationReportGeneratorLogByTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByTemplateParams) (database.NotificationReportGeneratorLog, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByUserAndTemplate", arg0, arg1) + ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByTemplate", arg0, arg1) ret0, _ := ret[0].(database.NotificationReportGeneratorLog) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetNotificationReportGeneratorLogByUserAndTemplate indicates an expected call of GetNotificationReportGeneratorLogByUserAndTemplate. -func (mr *MockStoreMockRecorder) GetNotificationReportGeneratorLogByUserAndTemplate(arg0, arg1 any) *gomock.Call { +// GetNotificationReportGeneratorLogByTemplate indicates an expected call of GetNotificationReportGeneratorLogByTemplate. +func (mr *MockStoreMockRecorder) GetNotificationReportGeneratorLogByTemplate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationReportGeneratorLogByUserAndTemplate", reflect.TypeOf((*MockStore)(nil).GetNotificationReportGeneratorLogByUserAndTemplate), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationReportGeneratorLogByTemplate", reflect.TypeOf((*MockStore)(nil).GetNotificationReportGeneratorLogByTemplate), arg0, arg1) } // GetNotificationTemplateByID mocks base method. diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 5d367ef14a249..64e7a30906080 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -718,7 +718,6 @@ CREATE TABLE notification_preferences ( ); CREATE TABLE notification_report_generator_logs ( - user_id uuid NOT NULL, notification_template_id uuid NOT NULL, last_generated_at timestamp with time zone NOT NULL ); @@ -1697,7 +1696,7 @@ ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id); ALTER TABLE ONLY notification_report_generator_logs - ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); + ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (notification_template_id); ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name); diff --git a/coderd/database/migrations/000250_email_reports.up.sql b/coderd/database/migrations/000250_email_reports.up.sql index b4f99d20cd45a..0d77020451b46 100644 --- a/coderd/database/migrations/000250_email_reports.up.sql +++ b/coderd/database/migrations/000250_email_reports.up.sql @@ -21,11 +21,10 @@ We recommend reviewing these issues to ensure future builds are successful.', CREATE TABLE notification_report_generator_logs ( - user_id uuid NOT NULL, notification_template_id uuid NOT NULL, last_generated_at timestamp with time zone NOT NULL, - PRIMARY KEY (user_id, notification_template_id) + PRIMARY KEY (notification_template_id) ); COMMENT ON TABLE notification_report_generator_logs IS 'Log of generated reports for users.'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 021dc37088811..406ad7fbb0584 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -2194,7 +2194,6 @@ type NotificationPreference struct { // Log of generated reports for users. type NotificationReportGeneratorLog struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 116d667a43194..ef745274926d5 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -170,7 +170,7 @@ type sqlcQuerier interface { GetLogoURL(ctx context.Context) (string, error) GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) // Fetch the notification report generator log indicating recent activity. - GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (NotificationReportGeneratorLog, error) + GetNotificationReportGeneratorLogByTemplate(ctx context.Context, notificationTemplateID uuid.UUID) (NotificationReportGeneratorLog, error) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) GetNotificationsSettings(ctx context.Context) (string, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 75567ae901cf3..3e0b27af4b1b5 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.24.0 package database @@ -3704,26 +3704,20 @@ func (q *sqlQuerier) GetNotificationMessagesByStatus(ctx context.Context, arg Ge return items, nil } -const getNotificationReportGeneratorLogByUserAndTemplate = `-- name: GetNotificationReportGeneratorLogByUserAndTemplate :one +const getNotificationReportGeneratorLogByTemplate = `-- name: GetNotificationReportGeneratorLogByTemplate :one SELECT - user_id, notification_template_id, last_generated_at + notification_template_id, last_generated_at FROM notification_report_generator_logs WHERE - user_id = $1 - AND notification_template_id = $2 + notification_template_id = $1 ` -type GetNotificationReportGeneratorLogByUserAndTemplateParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` -} - // Fetch the notification report generator log indicating recent activity. -func (q *sqlQuerier) GetNotificationReportGeneratorLogByUserAndTemplate(ctx context.Context, arg GetNotificationReportGeneratorLogByUserAndTemplateParams) (NotificationReportGeneratorLog, error) { - row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByUserAndTemplate, arg.UserID, arg.NotificationTemplateID) +func (q *sqlQuerier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, notificationTemplateID uuid.UUID) (NotificationReportGeneratorLog, error) { + row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByTemplate, notificationTemplateID) var i NotificationReportGeneratorLog - err := row.Scan(&i.UserID, &i.NotificationTemplateID, &i.LastGeneratedAt) + err := row.Scan(&i.NotificationTemplateID, &i.LastGeneratedAt) return i, err } @@ -3877,20 +3871,19 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg } const upsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec -INSERT INTO notification_report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES ($1, $2, $3) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id +INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES ($1, $2) +ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at +WHERE report_generator_logs.notification_template_id = EXCLUDED.notification_template_id ` type UpsertNotificationReportGeneratorLogParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"` } // Insert or update notification report generator logs with recent activity. func (q *sqlQuerier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error { - _, err := q.db.ExecContext(ctx, upsertNotificationReportGeneratorLog, arg.UserID, arg.NotificationTemplateID, arg.LastGeneratedAt) + _, err := q.db.ExecContext(ctx, upsertNotificationReportGeneratorLog, arg.NotificationTemplateID, arg.LastGeneratedAt) return err } diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 287ed9959f878..3adfbd942c4ab 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -175,21 +175,20 @@ FROM notification_templates WHERE kind = @kind::notification_template_kind ORDER BY name ASC; --- name: GetNotificationReportGeneratorLogByUserAndTemplate :one +-- name: GetNotificationReportGeneratorLogByTemplate :one -- Fetch the notification report generator log indicating recent activity. SELECT * FROM notification_report_generator_logs WHERE - user_id = $1 - AND notification_template_id = $2; + notification_template_id = $1; -- name: UpsertNotificationReportGeneratorLog :exec -- Insert or update notification report generator logs with recent activity. -INSERT INTO notification_report_generator_logs (user_id, notification_template_id, last_generated_at) VALUES (@user_id, @notification_template_id, @last_generated_at) -ON CONFLICT (user_id, notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE report_generator_logs.user_id = EXCLUDED.user_id AND report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; +INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES (@notification_template_id, @last_generated_at) +ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at +WHERE report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; -- name: DeleteOldNotificationReportGeneratorLogs :exec -- Delete report generator logs that have been created at least a @before date. diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 86bfa82d82932..3975550faa73a 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -25,7 +25,7 @@ const ( UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id); UniqueNotificationMessagesPkey UniqueConstraint = "notification_messages_pkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id); UniqueNotificationPreferencesPkey UniqueConstraint = "notification_preferences_pkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id); - UniqueNotificationReportGeneratorLogsPkey UniqueConstraint = "notification_report_generator_logs_pkey" // ALTER TABLE ONLY notification_report_generator_logs ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (user_id, notification_template_id); + UniqueNotificationReportGeneratorLogsPkey UniqueConstraint = "notification_report_generator_logs_pkey" // ALTER TABLE ONLY notification_report_generator_logs ADD CONSTRAINT notification_report_generator_logs_pkey PRIMARY KEY (notification_template_id); UniqueNotificationTemplatesNameKey UniqueConstraint = "notification_templates_name_key" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name); UniqueNotificationTemplatesPkey UniqueConstraint = "notification_templates_pkey" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_pkey PRIMARY KEY (id); UniqueOauth2ProviderAppCodesPkey UniqueConstraint = "oauth2_provider_app_codes_pkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_pkey PRIMARY KEY (id); diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 2a59e09e45a32..4d1d02570f73c 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -137,7 +137,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { - reportLog, err := db.GetNotificationReportGeneratorLogByUserAndTemplate(ctx, database.GetNotificationReportGeneratorLogByUserAndTemplateParams{ + reportLog, err := db.GetNotificationReportGeneratorLogByTemplate(ctx, database.GetNotificationReportGeneratorLogByTemplateParams{ UserID: templateAdmin.ID, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, }) From 8f634a481564f0f146d9cb9853c481578fb64a99 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 09:51:59 +0000 Subject: [PATCH 106/122] WIP --- coderd/database/dbmock/dbmock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 450b6d209cc96..02af41feac66b 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1553,7 +1553,7 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any) } // GetNotificationReportGeneratorLogByTemplate mocks base method. -func (m *MockStore) GetNotificationReportGeneratorLogByTemplate(arg0 context.Context, arg1 database.GetNotificationReportGeneratorLogByTemplateParams) (database.NotificationReportGeneratorLog, error) { +func (m *MockStore) GetNotificationReportGeneratorLogByTemplate(arg0 context.Context, arg1 uuid.UUID) (database.NotificationReportGeneratorLog, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetNotificationReportGeneratorLogByTemplate", arg0, arg1) ret0, _ := ret[0].(database.NotificationReportGeneratorLog) From 5215c92f966cc762c2d237b275d73f1eff4cd414 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 11:54:16 +0200 Subject: [PATCH 107/122] WIP --- coderd/database/dbmem/dbmem.go | 6 +++--- coderd/database/queries/notifications.sql | 2 +- coderd/notifications/reports/generator.go | 6 +----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8d0e73bdf8d04..2d3050e4044c1 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3002,8 +3002,8 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat return out, nil } -func (q *FakeQuerier) GetNotificationReportGeneratorLogByTemplate(_ context.Context, arg uuid.UUID) (database.NotificationReportGeneratorLog, error) { - err := validateDatabaseType(arg) +func (q *FakeQuerier) GetNotificationReportGeneratorLogByTemplate(_ context.Context, templateID uuid.UUID) (database.NotificationReportGeneratorLog, error) { + err := validateDatabaseType(templateID) if err != nil { return database.NotificationReportGeneratorLog{}, err } @@ -3012,7 +3012,7 @@ func (q *FakeQuerier) GetNotificationReportGeneratorLogByTemplate(_ context.Cont defer q.mutex.RUnlock() for _, record := range q.notificationReportGeneratorLogs { - if record.UserID == arg.UserID && record.NotificationTemplateID == arg.NotificationTemplateID { + if record.NotificationTemplateID == templateID { return record, nil } } diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 3adfbd942c4ab..1806af912937d 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -182,7 +182,7 @@ SELECT FROM notification_report_generator_logs WHERE - notification_template_id = $1; + notification_template_id = @template_id::uuid; -- name: UpsertNotificationReportGeneratorLog :exec -- Insert or update notification report generator logs with recent activity. diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 4d1d02570f73c..2a5d29e4d8a2a 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -137,10 +137,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { - reportLog, err := db.GetNotificationReportGeneratorLogByTemplate(ctx, database.GetNotificationReportGeneratorLogByTemplateParams{ - UserID: templateAdmin.ID, - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - }) + reportLog, err := db.GetNotificationReportGeneratorLogByTemplate(ctx, notifications.TemplateWorkspaceBuildsFailedReport) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { // sql.ErrNoRows: report not generated yet continue } @@ -178,7 +175,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat for u := range processedUsers { err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ - UserID: u, NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, LastGeneratedAt: dbtime.Time(now).UTC(), }) From 2edc7d54e7fbb40a12d3f808db9fa426450ef107 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 09:58:24 +0000 Subject: [PATCH 108/122] fix --- coderd/database/models.go | 2 +- coderd/database/querier.go | 4 ++-- coderd/database/queries.sql.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index 406ad7fbb0584..89facb4f67a8c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index ef745274926d5..ca835775c6f4c 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database @@ -170,7 +170,7 @@ type sqlcQuerier interface { GetLogoURL(ctx context.Context) (string, error) GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) // Fetch the notification report generator log indicating recent activity. - GetNotificationReportGeneratorLogByTemplate(ctx context.Context, notificationTemplateID uuid.UUID) (NotificationReportGeneratorLog, error) + GetNotificationReportGeneratorLogByTemplate(ctx context.Context, templateID uuid.UUID) (NotificationReportGeneratorLog, error) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) GetNotificationsSettings(ctx context.Context) (string, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3e0b27af4b1b5..f8042ed27b497 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database @@ -3710,12 +3710,12 @@ SELECT FROM notification_report_generator_logs WHERE - notification_template_id = $1 + notification_template_id = $1::uuid ` // Fetch the notification report generator log indicating recent activity. -func (q *sqlQuerier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, notificationTemplateID uuid.UUID) (NotificationReportGeneratorLog, error) { - row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByTemplate, notificationTemplateID) +func (q *sqlQuerier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, templateID uuid.UUID) (NotificationReportGeneratorLog, error) { + row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByTemplate, templateID) var i NotificationReportGeneratorLog err := row.Scan(&i.NotificationTemplateID, &i.LastGeneratedAt) return i, err From 53f6c63a5de5ef3d4685f3f8408056ae939490e4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 13:15:55 +0200 Subject: [PATCH 109/122] refactored --- coderd/database/dbmem/dbmem.go | 14 --- coderd/database/queries/notifications.sql | 4 - coderd/notifications/reports/generator.go | 105 +++++++++++----------- 3 files changed, 52 insertions(+), 71 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2d3050e4044c1..1d4bed0005b53 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1690,20 +1690,6 @@ func (*FakeQuerier) DeleteOldNotificationMessages(_ context.Context) error { return nil } -func (q *FakeQuerier) DeleteOldNotificationReportGeneratorLogs(_ context.Context, params database.DeleteOldNotificationReportGeneratorLogsParams) error { - q.mutex.Lock() - defer q.mutex.Unlock() - - var validLogs []database.NotificationReportGeneratorLog - for _, record := range q.notificationReportGeneratorLogs { - if record.NotificationTemplateID != params.NotificationTemplateID || record.LastGeneratedAt.After(params.Before) { - validLogs = append(validLogs, record) - } - } - q.notificationReportGeneratorLogs = validLogs - return nil -} - func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index 1806af912937d..a48e555a35c36 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -189,7 +189,3 @@ WHERE INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES (@notification_template_id, @last_generated_at) ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at WHERE report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; - --- name: DeleteOldNotificationReportGeneratorLogs :exec --- Delete report generator logs that have been created at least a @before date. -DELETE FROM notification_report_generator_logs WHERE last_generated_at < @before::timestamptz AND notification_template_id = @notification_template_id; diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 2a5d29e4d8a2a..f97f6c87eedf8 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -106,59 +106,67 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat now := clk.Now() since := now.Add(-failedWorkspaceBuildsReportFrequency) - statsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(since).UTC()) - if err != nil { - return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) + // Firstly, check if this is the first run of the job ever + reportLog, err := db.GetNotificationReportGeneratorLogByTemplate(ctx, notifications.TemplateWorkspaceBuildsFailedReport) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("unable to read report generator log: %w", err) } + if xerrors.Is(err, sql.ErrNoRows) { + // First run? Check-in the job, and get back after one week. + logger.Info(ctx, "report generator is executing the job for the first time", slog.F("notification_template_id", notifications.TemplateWorkspaceBuildsFailedReport)) - processedUsers := map[uuid.UUID]bool{} - for _, stats := range statsRows { - var failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow - reportData := map[string]any{} + err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ + NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, + LastGeneratedAt: dbtime.Time(now).UTC(), + }) + if err != nil { + return xerrors.Errorf("unable to update report generator logs (first time execution): %w", err) + } + return nil + } - if stats.FailedBuilds > 0 { - failedBuilds, err = db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ - TemplateID: stats.TemplateID, - Since: dbtime.Time(since).UTC(), - }) - if err != nil { - logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err)) - continue - } + // Secondly, check if the job has not been running recently + if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequency).After(now) { + return nil // reports sent recently, no need to send them now + } + + // Thirdly, fetch workspace build stats by templates + templateStatsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(since).UTC()) + if err != nil { + return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) + } - // There are some failed builds, so we have to prepare input data for the report. - reportData = buildDataForReportFailedWorkspaceBuilds(stats, failedBuilds) + for _, stats := range templateStatsRows { + if stats.FailedBuilds == 0 { + logger.Error(ctx, "no failed workspace builds found for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) + continue } + // Fetch template admins with org access to the templates templateAdmins, err := findTemplateAdmins(ctx, db, stats) if err != nil { - logger.Error(ctx, "unable to find template admins", slog.F("template_id", stats.TemplateID), slog.Error(err)) + logger.Error(ctx, "unable to find template admins for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) continue } - for _, templateAdmin := range templateAdmins { - reportLog, err := db.GetNotificationReportGeneratorLogByTemplate(ctx, notifications.TemplateWorkspaceBuildsFailedReport) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { // sql.ErrNoRows: report not generated yet - continue - } - - if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequency).After(now) { - // report generated recently, no need to send it now - continue - } - - processedUsers[templateAdmin.ID] = true - - if len(failedBuilds) == 0 { - // no failed workspace builds, no need to send the report - continue - } + // Fetch failed builds by the template + failedBuilds, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ + TemplateID: stats.TemplateID, + Since: dbtime.Time(since).UTC(), + }) + if err != nil { + logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err)) + continue + } + reportData := buildDataForReportFailedWorkspaceBuilds(stats, failedBuilds) - templateDisplayName := stats.TemplateDisplayName - if templateDisplayName == "" { - templateDisplayName = stats.TemplateName - } + // Send reports to template admins + templateDisplayName := stats.TemplateDisplayName + if templateDisplayName == "" { + templateDisplayName = stats.TemplateName + } + for _, templateAdmin := range templateAdmins { if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, map[string]string{ "template_name": stats.TemplateName, @@ -173,22 +181,13 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } - for u := range processedUsers { - err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Time(now).UTC(), - }) - if err != nil { - logger.Error(ctx, "unable to update report generator logs", slog.F("user_id", u), slog.Error(err)) - } - } - - err = db.DeleteOldNotificationReportGeneratorLogs(ctx, database.DeleteOldNotificationReportGeneratorLogsParams{ + // Lastly, update the timestamp in the generator log. + err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - Before: dbtime.Time(now.Add(-failedWorkspaceBuildsReportFrequency - time.Hour)).UTC(), + LastGeneratedAt: dbtime.Time(now).UTC(), }) if err != nil { - return xerrors.Errorf("unable to delete old report generator logs: %w", err) + return xerrors.Errorf("unable to update report generator logs: %w", err) } return nil } From 6405f3c64bfaa71c451f0c676de8014dfbf61390 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 11:16:53 +0000 Subject: [PATCH 110/122] WIP --- coderd/database/dbauthz/dbauthz.go | 14 +++++++------- coderd/database/dbmetrics/dbmetrics.go | 14 +++++++------- coderd/database/dbmock/dbmock.go | 14 -------------- coderd/database/querier.go | 2 -- coderd/database/queries.sql.go | 15 --------------- 5 files changed, 14 insertions(+), 45 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 908045a367863..b0d87da53328b 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -898,6 +898,13 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } +func (q *querier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg database.DeleteOldNotificationReportGeneratorLogsParams) error { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { + return err + } + return q.db.DeleteOldNotificationReportGeneratorLogs(ctx, arg) +} + func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } @@ -1137,13 +1144,6 @@ func (q *querier) DeleteOldNotificationMessages(ctx context.Context) error { return q.db.DeleteOldNotificationMessages(ctx) } -func (q *querier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg database.DeleteOldNotificationReportGeneratorLogsParams) error { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { - return err - } - return q.db.DeleteOldNotificationReportGeneratorLogs(ctx, arg) -} - func (q *querier) DeleteOldProvisionerDaemons(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index cdae7bc96478a..882174511fdee 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -81,6 +81,13 @@ func (m metricsStore) InTx(f func(database.Store) error, options *sql.TxOptions) return err } +func (m metricsStore) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldNotificationReportGeneratorLogsParams) error { + start := time.Now() + r0 := m.s.DeleteOldNotificationReportGeneratorLogs(ctx, frequencyDays) + m.queryLatencies.WithLabelValues("DeleteOldNotificationReportGeneratorLogs").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error { start := time.Now() err := m.s.AcquireLock(ctx, pgAdvisoryXactLock) @@ -298,13 +305,6 @@ func (m metricsStore) DeleteOldNotificationMessages(ctx context.Context) error { return r0 } -func (m metricsStore) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldNotificationReportGeneratorLogsParams) error { - start := time.Now() - r0 := m.s.DeleteOldNotificationReportGeneratorLogs(ctx, frequencyDays) - m.queryLatencies.WithLabelValues("DeleteOldNotificationReportGeneratorLogs").Observe(time.Since(start).Seconds()) - return r0 -} - func (m metricsStore) DeleteOldProvisionerDaemons(ctx context.Context) error { start := time.Now() r0 := m.s.DeleteOldProvisionerDaemons(ctx) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 02af41feac66b..ebd926aef4e75 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -486,20 +486,6 @@ func (mr *MockStoreMockRecorder) DeleteOldNotificationMessages(arg0 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNotificationMessages", reflect.TypeOf((*MockStore)(nil).DeleteOldNotificationMessages), arg0) } -// DeleteOldNotificationReportGeneratorLogs mocks base method. -func (m *MockStore) DeleteOldNotificationReportGeneratorLogs(arg0 context.Context, arg1 database.DeleteOldNotificationReportGeneratorLogsParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOldNotificationReportGeneratorLogs", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteOldNotificationReportGeneratorLogs indicates an expected call of DeleteOldNotificationReportGeneratorLogs. -func (mr *MockStoreMockRecorder) DeleteOldNotificationReportGeneratorLogs(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNotificationReportGeneratorLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldNotificationReportGeneratorLogs), arg0, arg1) -} - // DeleteOldProvisionerDaemons mocks base method. func (m *MockStore) DeleteOldProvisionerDaemons(arg0 context.Context) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index ca835775c6f4c..e5cffeecbc118 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -82,8 +82,6 @@ type sqlcQuerier interface { DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error // Delete all notification messages which have not been updated for over a week. DeleteOldNotificationMessages(ctx context.Context) error - // Delete report generator logs that have been created at least a @before date. - DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error // Delete provisioner daemons that have been created at least a week ago // and have not connected to coderd since a week. // A provisioner daemon with "zeroed" last_seen_at column indicates possible diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f8042ed27b497..536b9ad6f6dd0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3552,21 +3552,6 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error { return err } -const deleteOldNotificationReportGeneratorLogs = `-- name: DeleteOldNotificationReportGeneratorLogs :exec -DELETE FROM notification_report_generator_logs WHERE last_generated_at < $1::timestamptz AND notification_template_id = $2 -` - -type DeleteOldNotificationReportGeneratorLogsParams struct { - Before time.Time `db:"before" json:"before"` - NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"` -} - -// Delete report generator logs that have been created at least a @before date. -func (q *sqlQuerier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg DeleteOldNotificationReportGeneratorLogsParams) error { - _, err := q.db.ExecContext(ctx, deleteOldNotificationReportGeneratorLogs, arg.Before, arg.NotificationTemplateID) - return err -} - const enqueueNotificationMessage = `-- name: EnqueueNotificationMessage :exec INSERT INTO notification_messages (id, notification_template_id, user_id, method, payload, targets, created_by, created_at) VALUES ($1, From d96a34a10fec77b8304ce7d8cac2f07ad941b3e5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 13:17:42 +0200 Subject: [PATCH 111/122] refactored --- coderd/database/dbauthz/dbauthz.go | 7 ------- coderd/database/dbauthz/dbauthz_test.go | 6 ------ coderd/database/dbmetrics/dbmetrics.go | 7 ------- 3 files changed, 20 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index b0d87da53328b..532aadd5d0dbf 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -898,13 +898,6 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) return nil } -func (q *querier) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, arg database.DeleteOldNotificationReportGeneratorLogsParams) error { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { - return err - } - return q.db.DeleteOldNotificationReportGeneratorLogs(ctx, arg) -} - func (q *querier) AcquireLock(ctx context.Context, id int64) error { return q.db.AcquireLock(ctx, id) } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 1419f8ed296d9..87dc52591f365 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2735,12 +2735,6 @@ func (s *MethodTestSuite) TestSystemFunctions() { Value: "value", }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("DeleteOldNotificationReportGeneratorLogs", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.DeleteOldNotificationReportGeneratorLogsParams{ - Before: dbtime.Now(), - NotificationTemplateID: uuid.New(), - }).Asserts(rbac.ResourceSystem, policy.ActionDelete) - })) s.Run("GetFailedWorkspaceBuildsByTemplateID", s.Subtest(func(db database.Store, check *expects) { check.Args(database.GetFailedWorkspaceBuildsByTemplateIDParams{ TemplateID: uuid.New(), diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 882174511fdee..8b09955e01347 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -81,13 +81,6 @@ func (m metricsStore) InTx(f func(database.Store) error, options *sql.TxOptions) return err } -func (m metricsStore) DeleteOldNotificationReportGeneratorLogs(ctx context.Context, frequencyDays database.DeleteOldNotificationReportGeneratorLogsParams) error { - start := time.Now() - r0 := m.s.DeleteOldNotificationReportGeneratorLogs(ctx, frequencyDays) - m.queryLatencies.WithLabelValues("DeleteOldNotificationReportGeneratorLogs").Observe(time.Since(start).Seconds()) - return r0 -} - func (m metricsStore) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error { start := time.Now() err := m.s.AcquireLock(ctx, pgAdvisoryXactLock) From 06a79f47f779aa272b8f8253f8818cf8a5d02b81 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 13:50:18 +0200 Subject: [PATCH 112/122] adjust tests --- coderd/notifications/reports/generator.go | 2 +- .../reports/generator_internal_test.go | 119 ++++++++++++++---- 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index f97f6c87eedf8..3d3e43dcf46e4 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -138,7 +138,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat for _, stats := range templateStatsRows { if stats.FailedBuilds == 0 { - logger.Error(ctx, "no failed workspace builds found for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) + logger.Info(ctx, "no failed workspace builds found for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) continue } diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index ba0239cf527e7..d7fdba0e764e9 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -35,7 +35,7 @@ var ( func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Parallel() - t.Run("InitialState_NoBuilds_NoReport", func(t *testing.T) { + t.Run("EmptyState_NoBuilds_NoReport", func(t *testing.T) { t.Parallel() // Setup @@ -44,10 +44,73 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Database is ready, so we can clear notifications queue notifEnq.Clear() + // When: first run + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then: no report should be generated + require.NoError(t, err) + require.Empty(t, notifEnq.Sent) + + // Given: one week later and no jobs were executed + clk.Advance(failedWorkspaceBuildsReportFrequency + time.Minute) + // When + notifEnq.Clear() + err = reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then: report is still empty + require.NoError(t, err) + require.Empty(t, notifEnq.Sent) + }) + + t.Run("InitialState_NoBuilds_NoReport", func(t *testing.T) { + t.Parallel() + + // Setup + ctx, logger, db, ps, notifEnq, clk := setup(t) + now := clk.Now() + + // Organization + org := dbgen.Organization(t, db, database.Organization{}) + + // Template admins + templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID}) + + // Regular users + user1 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) + user2 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user2.ID, OrganizationID: org.ID}) + + // Templates + t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) + + // Template versions + t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + + // Workspaces + w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) + + w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-2 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + // When: first run + notifEnq.Clear() err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) - // Then + // Then: failed builds should not be reported + require.NoError(t, err) + require.Empty(t, notifEnq.Sent) + + // Given: one week later, but still no jobs + clk.Advance(failedWorkspaceBuildsReportFrequency + time.Minute) + + // When + notifEnq.Clear() + err = reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then: report is still empty require.NoError(t, err) require.Empty(t, notifEnq.Sent) }) @@ -72,6 +135,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { ctx, logger, db, ps, notifEnq, clk := setup(t) // Given + // Organization org := dbgen.Organization(t, db, database.Organization{}) @@ -105,6 +169,16 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { w3 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) w4 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t2.ID, OwnerID: user2.ID, OrganizationID: org.ID}) + // When: first run + notifEnq.Clear() + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then + require.NoError(t, err) + require.Empty(t, notifEnq.Sent) // no notifications + + // One week later... + clk.Advance(failedWorkspaceBuildsReportFrequency + time.Minute) now := clk.Now() // Workspace builds @@ -130,11 +204,9 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { w4wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-dayDuration), Valid: true}}) _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w4.ID, BuildNumber: 9, TemplateVersionID: t2v2.ID, JobID: w4wb2pj.ID, CreatedAt: now.Add(-dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) - // Database is ready, so we can clear notifications queue - notifEnq.Clear() - // When - err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) + notifEnq.Clear() + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) // Then require.NoError(t, err) @@ -182,30 +254,28 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Given: 6 days later (less than report frequency), and failed build clk.Advance(6 * dayDuration).MustWait(context.Background()) - now = clk.Now() w1wb4pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: now.Add(-dayDuration), Valid: true}}) _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 77, TemplateVersionID: t1v2.ID, JobID: w1wb4pj.ID, CreatedAt: now.Add(-dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) - notifEnq.Clear() - // When + notifEnq.Clear() err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) require.NoError(t, err) - // Then - require.Empty(t, notifEnq.Sent) // no notifications as it is too early. + // Then: no notifications as it is too early + require.Empty(t, notifEnq.Sent) // Given: 1 day 1 hour later clk.Advance(dayDuration + time.Hour).MustWait(context.Background()) - notifEnq.Clear() // When + notifEnq.Clear() err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) require.NoError(t, err) - // Then + // Then: we should see the failed job in the report require.Len(t, notifEnq.Sent, 2) // a new failed job should be reported for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { verifyNotification(t, templateAdmin, notifEnq.Sent[i], t1, 1, 1, []map[string]interface{}{ @@ -247,23 +317,30 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Workspaces w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) + // When: first run + notifEnq.Clear() + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then: no notifications + require.NoError(t, err) + require.Empty(t, notifEnq.Sent) + + // Given: one week later, and a successful few jobs being executed + clk.Advance(failedWorkspaceBuildsReportFrequency + time.Minute) now := clk.Now() // Workspace builds w1wb1pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-6 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-6 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 1, TemplateVersionID: t1v1.ID, JobID: w1wb1pj.ID, CreatedAt: now.Add(-2 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) w1wb2pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-5 * dayDuration), Valid: true}}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v1.ID, JobID: w1wb2pj.ID, CreatedAt: now.Add(-5 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) - - // Database is ready, so we can clear notifications queue - notifEnq.Clear() + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 2, TemplateVersionID: t1v1.ID, JobID: w1wb2pj.ID, CreatedAt: now.Add(-1 * dayDuration), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) // When - err := reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) + notifEnq.Clear() + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) - // Then + // Then: no failures? nothing to report require.NoError(t, err) - require.Len(t, notifEnq.Sent, 0) // all jobs succeeded so nothing to report }) } From 0a05a407e69a9580a1d856f453999989cffd28ab Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 13:55:17 +0200 Subject: [PATCH 113/122] duplicate migration --- ...00250_email_reports.down.sql => 000251_email_reports.down.sql} | 0 .../{000250_email_reports.up.sql => 000251_email_reports.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000250_email_reports.down.sql => 000251_email_reports.down.sql} (100%) rename coderd/database/migrations/{000250_email_reports.up.sql => 000251_email_reports.up.sql} (100%) diff --git a/coderd/database/migrations/000250_email_reports.down.sql b/coderd/database/migrations/000251_email_reports.down.sql similarity index 100% rename from coderd/database/migrations/000250_email_reports.down.sql rename to coderd/database/migrations/000251_email_reports.down.sql diff --git a/coderd/database/migrations/000250_email_reports.up.sql b/coderd/database/migrations/000251_email_reports.up.sql similarity index 100% rename from coderd/database/migrations/000250_email_reports.up.sql rename to coderd/database/migrations/000251_email_reports.up.sql From 65cf4a8306f5ac0cdbb9051d8dc1cb2468ad5ec4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 13:56:17 +0200 Subject: [PATCH 114/122] dbauthz --- coderd/database/dbauthz/dbauthz_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 923925954156f..1a6aff5b111f1 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2776,9 +2776,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, LastGeneratedAt: dbtime.Now(), }) - check.Args(database.GetNotificationReportGeneratorLogByTemplateParams{ - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - }).Asserts(rbac.ResourceSystem, policy.ActionRead) + check.Args(notifications.TemplateWorkspaceBuildsFailedReport).Asserts(rbac.ResourceSystem, policy.ActionRead) })) s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) { check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) From 898856d93fd0e0cb7425920161b32403a1f0bafc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 14:00:20 +0200 Subject: [PATCH 115/122] fix --- coderd/database/queries/notifications.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries/notifications.sql b/coderd/database/queries/notifications.sql index a48e555a35c36..f2d1a14c3aae7 100644 --- a/coderd/database/queries/notifications.sql +++ b/coderd/database/queries/notifications.sql @@ -188,4 +188,4 @@ WHERE -- Insert or update notification report generator logs with recent activity. INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES (@notification_template_id, @last_generated_at) ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; +WHERE notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id; From a5e42da6dddddb8459ebb09eea116cadaf2e20f4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 12:01:59 +0000 Subject: [PATCH 116/122] makegen --- coderd/database/queries.sql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6f832909472e8..1f8f819434509 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3868,7 +3868,7 @@ func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg const upsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES ($1, $2) ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at -WHERE report_generator_logs.notification_template_id = EXCLUDED.notification_template_id +WHERE notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id ` type UpsertNotificationReportGeneratorLogParams struct { From 72724b36c7cf9eafa0dd8a4ec50e9fb8b9500946 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 14:16:03 +0200 Subject: [PATCH 117/122] fix --- coderd/notifications/reports/generator.go | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 3d3e43dcf46e4..c3357d292c861 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -51,7 +51,6 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueuer, clk) if err != nil { - logger.Debug(ctx, "unable to generate reports with failed workspace builds") return xerrors.Errorf("unable to generate reports with failed workspace builds: %w", err) } @@ -137,6 +136,11 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, stats := range templateStatsRows { + if ctx.Err() == context.Canceled { + logger.Debug(ctx, "context is canceled, quitting") + break + } + if stats.FailedBuilds == 0 { logger.Info(ctx, "no failed workspace builds found for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) continue @@ -167,6 +171,11 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { + if ctx.Err() == context.Canceled { + logger.Debug(ctx, "context is canceled, quitting") + break + } + if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, map[string]string{ "template_name": stats.TemplateName, @@ -181,6 +190,11 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } + if ctx.Err() == context.Canceled { + logger.Error(ctx, "report generator job is canceled") + return ctx.Err() + } + // Lastly, update the timestamp in the generator log. err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, @@ -193,14 +207,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any { - // Sorting order: template_version_name ASC, workspace build number DESC - sort.Slice(failedBuilds, func(i, j int) bool { - if failedBuilds[i].TemplateVersionName != failedBuilds[j].TemplateVersionName { - return failedBuilds[i].TemplateVersionName < failedBuilds[j].TemplateVersionName - } - return failedBuilds[i].WorkspaceBuildNumber > failedBuilds[j].WorkspaceBuildNumber - }) - // Build notification model for template versions and failed workspace builds. // // Failed builds are sorted by template version ascending, workspace build number descending. From 20abd6903442f0c4039cccb52378ba32b241b840 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 14:16:55 +0200 Subject: [PATCH 118/122] fix: migration down --- coderd/database/migrations/000251_email_reports.down.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/migrations/000251_email_reports.down.sql b/coderd/database/migrations/000251_email_reports.down.sql index 7a39df5a02753..ab45123bcd53b 100644 --- a/coderd/database/migrations/000251_email_reports.down.sql +++ b/coderd/database/migrations/000251_email_reports.down.sql @@ -1,3 +1,3 @@ DELETE FROM notification_templates WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00'; -DROP TABLE report_generator_logs; +DROP TABLE notification_report_generator_logs; From b2df714baa3e7b558ab0c71682b03feab0e2fe35 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 14:29:19 +0200 Subject: [PATCH 119/122] xerrors.Is --- coderd/notifications/reports/generator.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index c3357d292c861..6dfb989fc4c7b 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -136,7 +136,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, stats := range templateStatsRows { - if ctx.Err() == context.Canceled { + if xerrors.Is(ctx.Err(), context.Canceled) { logger.Debug(ctx, "context is canceled, quitting") break } @@ -171,7 +171,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { - if ctx.Err() == context.Canceled { + if xerrors.Is(ctx.Err(), context.Canceled) { logger.Debug(ctx, "context is canceled, quitting") break } @@ -190,7 +190,7 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } } - if ctx.Err() == context.Canceled { + if xerrors.Is(ctx.Err(), context.Canceled) { logger.Error(ctx, "report generator job is canceled") return ctx.Err() } From 65992f6e984af575274ffbc9d626fa097aa0c15e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 14:51:40 +0200 Subject: [PATCH 120/122] limit builds --- coderd/notifications/reports/generator.go | 22 ++-- .../reports/generator_internal_test.go | 114 +++++++++++++++++- 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 6dfb989fc4c7b..aebbebeb8225a 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -206,6 +206,8 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return nil } +const workspaceBuildsLimitPerTemplateVersion = 10 + func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any { // Build notification model for template versions and failed workspace builds. // @@ -233,15 +235,19 @@ func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildSta tv := templateVersions[c-1] //nolint:errorlint,forcetypeassert // only this function prepares the notification model - builds := tv["failed_builds"].([]map[string]any) - builds = append(builds, map[string]any{ - "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, - "workspace_name": failedBuild.WorkspaceName, - "build_number": failedBuild.WorkspaceBuildNumber, - }) - tv["failed_builds"] = builds - //nolint:errorlint,forcetypeassert // only this function prepares the notification model tv["failed_count"] = tv["failed_count"].(int) + 1 + + //nolint:errorlint,forcetypeassert // only this function prepares the notification model + builds := tv["failed_builds"].([]map[string]any) + if len(builds) < workspaceBuildsLimitPerTemplateVersion { + // return N last builds to prevent long email reports + builds = append(builds, map[string]any{ + "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, + "workspace_name": failedBuild.WorkspaceName, + "build_number": failedBuild.WorkspaceBuildNumber, + }) + tv["failed_builds"] = builds + } templateVersions[c-1] = tv } diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index d7fdba0e764e9..a6a7f66f725cf 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -115,7 +115,7 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.Empty(t, notifEnq.Sent) }) - t.Run("FailedBuilds_FirstRun_Report_SecondRunTooEarly_NoReport_ThirdRun_Report", func(t *testing.T) { + t.Run("FailedBuilds_SecondRun_Report_ThirdRunTooEarly_NoReport_FourthRun_Report", func(t *testing.T) { t.Parallel() verifyNotification := func(t *testing.T, recipient database.User, notif *testutil.Notification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) { @@ -290,6 +290,118 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { } }) + t.Run("TooManyFailedBuilds_SecondRun_Report", func(t *testing.T) { + t.Parallel() + + verifyNotification := func(t *testing.T, recipient database.User, notif *testutil.Notification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) { + t.Helper() + + require.Equal(t, recipient.ID, notif.UserID) + require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID) + require.Equal(t, tmpl.Name, notif.Labels["template_name"]) + require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"]) + require.Equal(t, failedBuilds, notif.Data["failed_builds"]) + require.Equal(t, totalBuilds, notif.Data["total_builds"]) + require.Equal(t, "week", notif.Data["report_frequency"]) + require.Equal(t, templateVersions, notif.Data["template_versions"]) + } + + // Setup + ctx, logger, db, ps, notifEnq, clk := setup(t) + + // Given + + // Organization + org := dbgen.Organization(t, db, database.Organization{}) + + // Template admins + templateAdmin1 := dbgen.User(t, db, database.User{Username: "template-admin-1", RBACRoles: []string{rbac.RoleTemplateAdmin().Name}}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: templateAdmin1.ID, OrganizationID: org.ID}) + + // Regular users + user1 := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user1.ID, OrganizationID: org.ID}) + + // Templates + t1 := dbgen.Template(t, db, database.Template{Name: "template-1", DisplayName: "First Template", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID}) + + // Template versions + t1v1 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-1", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + t1v2 := dbgen.TemplateVersion(t, db, database.TemplateVersion{Name: "template-1-version-2", CreatedBy: templateAdmin1.ID, OrganizationID: org.ID, TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, JobID: uuid.New()}) + + // Workspaces + w1 := dbgen.Workspace(t, db, database.Workspace{TemplateID: t1.ID, OwnerID: user1.ID, OrganizationID: org.ID}) + + // When: first run + notifEnq.Clear() + err := reportFailedWorkspaceBuilds(ctx, logger, db, notifEnq, clk) + + // Then + require.NoError(t, err) + require.Empty(t, notifEnq.Sent) // no notifications + + // One week later... + clk.Advance(failedWorkspaceBuildsReportFrequency + time.Minute) + now := clk.Now() + + // Workspace builds + pj0 := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, CompletedAt: sql.NullTime{Time: now.Add(-24 * time.Hour), Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: 777, TemplateVersionID: t1v1.ID, JobID: pj0.ID, CreatedAt: now.Add(-24 * time.Hour), Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + for i := 1; i <= 23; i++ { + at := now.Add(-time.Duration(i) * time.Hour) + + pj1 := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: at, Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: int32(i), TemplateVersionID: t1v1.ID, JobID: pj1.ID, CreatedAt: at, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + + pj2 := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{OrganizationID: org.ID, Error: jobError, ErrorCode: jobErrorCode, CompletedAt: sql.NullTime{Time: at, Valid: true}}) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{WorkspaceID: w1.ID, BuildNumber: int32(i) + 100, TemplateVersionID: t1v2.ID, JobID: pj2.ID, CreatedAt: at, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator}) + } + + // When + notifEnq.Clear() + err = reportFailedWorkspaceBuilds(ctx, logger, authedDB(t, db, logger), notifEnq, clk) + + // Then + require.NoError(t, err) + + require.Len(t, notifEnq.Sent, 1) // 1 template, 1 template admin + verifyNotification(t, templateAdmin1, notifEnq.Sent[0], t1, 46, 47, []map[string]interface{}{ + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(23), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(22), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(21), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(20), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(19), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(18), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(17), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(16), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(15), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(14), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 23, + "template_version_name": t1v1.Name, + }, + { + "failed_builds": []map[string]interface{}{ + {"build_number": int32(123), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(122), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(121), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(120), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(119), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(118), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(117), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(116), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(115), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + {"build_number": int32(114), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + }, + "failed_count": 23, + "template_version_name": t1v2.Name, + }, + }) + }) + t.Run("NoFailedBuilds_NoReport", func(t *testing.T) { t.Parallel() From af678738fcc43d84ca037d03771f86c98989ddf4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Sep 2024 15:39:40 +0200 Subject: [PATCH 121/122] context cancelled --- coderd/notifications/reports/generator.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index aebbebeb8225a..b76ef5374bc15 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -136,9 +136,11 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, stats := range templateStatsRows { - if xerrors.Is(ctx.Err(), context.Canceled) { - logger.Debug(ctx, "context is canceled, quitting") + select { + case <-ctx.Done(): + logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err())) break + default: } if stats.FailedBuilds == 0 { @@ -171,9 +173,11 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat } for _, templateAdmin := range templateAdmins { - if xerrors.Is(ctx.Err(), context.Canceled) { - logger.Debug(ctx, "context is canceled, quitting") + select { + case <-ctx.Done(): + logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err())) break + default: } if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, From 6bd772ce87e6917a62cc5c48398b616f2d6a6bcc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 18 Sep 2024 08:57:16 +0200 Subject: [PATCH 122/122] fix migration --- ...00251_email_reports.down.sql => 000253_email_reports.down.sql} | 0 .../{000251_email_reports.up.sql => 000253_email_reports.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000251_email_reports.down.sql => 000253_email_reports.down.sql} (100%) rename coderd/database/migrations/{000251_email_reports.up.sql => 000253_email_reports.up.sql} (100%) diff --git a/coderd/database/migrations/000251_email_reports.down.sql b/coderd/database/migrations/000253_email_reports.down.sql similarity index 100% rename from coderd/database/migrations/000251_email_reports.down.sql rename to coderd/database/migrations/000253_email_reports.down.sql diff --git a/coderd/database/migrations/000251_email_reports.up.sql b/coderd/database/migrations/000253_email_reports.up.sql similarity index 100% rename from coderd/database/migrations/000251_email_reports.up.sql rename to coderd/database/migrations/000253_email_reports.up.sql