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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy