chore: enable coder inbox by default (#17077)

Add a flag to enable Coder Inbox by default, as well as supporting disabling the feature.
This commit is contained in:
Danielle Maywood
2025-03-25 12:51:26 +00:00
committed by GitHub
parent 5f3a53f01b
commit cd19e79d9b
15 changed files with 260 additions and 42 deletions

16
coderd/apidoc/docs.go generated
View File

@ -12658,6 +12658,14 @@ const docTemplate = `{
"description": "How often to query the database for queued notifications.",
"type": "integer"
},
"inbox": {
"description": "Inbox settings.",
"allOf": [
{
"$ref": "#/definitions/codersdk.NotificationsInboxConfig"
}
]
},
"lease_count": {
"description": "How many notifications a notifier should lease per fetch interval.",
"type": "integer"
@ -12783,6 +12791,14 @@ const docTemplate = `{
}
}
},
"codersdk.NotificationsInboxConfig": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"codersdk.NotificationsSettings": {
"type": "object",
"properties": {

View File

@ -11369,6 +11369,14 @@
"description": "How often to query the database for queued notifications.",
"type": "integer"
},
"inbox": {
"description": "Inbox settings.",
"allOf": [
{
"$ref": "#/definitions/codersdk.NotificationsInboxConfig"
}
]
},
"lease_count": {
"description": "How many notifications a notifier should lease per fetch interval.",
"type": "integer"
@ -11494,6 +11502,14 @@
}
}
},
"codersdk.NotificationsInboxConfig": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"codersdk.NotificationsSettings": {
"type": "object",
"properties": {

View File

@ -3,6 +3,7 @@ package notifications
import (
"context"
"encoding/json"
"slices"
"strings"
"text/template"
@ -28,7 +29,10 @@ type StoreEnqueuer struct {
store Store
log slog.Logger
defaultMethod database.NotificationMethod
defaultMethod database.NotificationMethod
defaultEnabled bool
inboxEnabled bool
// helpers holds a map of template funcs which are used when rendering templates. These need to be passed in because
// the template funcs will return values which are inappropriately encapsulated in this struct.
helpers template.FuncMap
@ -44,11 +48,13 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem
}
return &StoreEnqueuer{
store: store,
log: log,
defaultMethod: method,
helpers: helpers,
clock: clock,
store: store,
log: log,
defaultMethod: method,
defaultEnabled: cfg.Enabled(),
inboxEnabled: cfg.Inbox.Enabled.Value(),
helpers: helpers,
clock: clock,
}, nil
}
@ -69,11 +75,6 @@ func (s *StoreEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID
return nil, xerrors.Errorf("new message metadata: %w", err)
}
dispatchMethod := s.defaultMethod
if metadata.CustomMethod.Valid {
dispatchMethod = metadata.CustomMethod.NotificationMethod
}
payload, err := s.buildPayload(metadata, labels, data, targets)
if err != nil {
s.log.Warn(ctx, "failed to build payload", slog.F("template_id", templateID), slog.F("user_id", userID), slog.Error(err))
@ -85,11 +86,22 @@ func (s *StoreEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID
return nil, xerrors.Errorf("failed encoding input labels: %w", err)
}
uuids := make([]uuid.UUID, 0, 2)
methods := []database.NotificationMethod{}
if metadata.CustomMethod.Valid {
methods = append(methods, metadata.CustomMethod.NotificationMethod)
} else if s.defaultEnabled {
methods = append(methods, s.defaultMethod)
}
// All the enqueued messages are enqueued both on the dispatch method set by the user (or default one) and the inbox.
// As the inbox is not configurable per the user and is always enabled, we always enqueue the message on the inbox.
// The logic is done here in order to have two completely separated processing and retries are handled separately.
for _, method := range []database.NotificationMethod{dispatchMethod, database.NotificationMethodInbox} {
if !slices.Contains(methods, database.NotificationMethodInbox) && s.inboxEnabled {
methods = append(methods, database.NotificationMethodInbox)
}
uuids := make([]uuid.UUID, 0, 2)
for _, method := range methods {
id := uuid.New()
err = s.store.EnqueueNotificationMessage(ctx, database.EnqueueNotificationMessageParams{
ID: id,

View File

@ -1856,6 +1856,90 @@ func TestNotificationDuplicates(t *testing.T) {
require.NoError(t, err)
}
func TestNotificationTargetMatrix(t *testing.T) {
t.Parallel()
tests := []struct {
name string
defaultMethod database.NotificationMethod
defaultEnabled bool
inboxEnabled bool
expectedEnqueued int
}{
{
name: "NoDefaultAndNoInbox",
defaultMethod: database.NotificationMethodSmtp,
defaultEnabled: false,
inboxEnabled: false,
expectedEnqueued: 0,
},
{
name: "DefaultAndNoInbox",
defaultMethod: database.NotificationMethodSmtp,
defaultEnabled: true,
inboxEnabled: false,
expectedEnqueued: 1,
},
{
name: "NoDefaultAndInbox",
defaultMethod: database.NotificationMethodSmtp,
defaultEnabled: false,
inboxEnabled: true,
expectedEnqueued: 1,
},
{
name: "DefaultAndInbox",
defaultMethod: database.NotificationMethodSmtp,
defaultEnabled: true,
inboxEnabled: true,
expectedEnqueued: 2,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// nolint:gocritic // Unit test.
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
store, pubsub := dbtestutil.NewDB(t)
logger := testutil.Logger(t)
cfg := defaultNotificationsConfig(tt.defaultMethod)
cfg.Inbox.Enabled = serpent.Bool(tt.inboxEnabled)
// If the default method is not enabled, we want to ensure the config
// is wiped out.
if !tt.defaultEnabled {
cfg.SMTP = codersdk.NotificationsEmailConfig{}
cfg.Webhook = codersdk.NotificationsWebhookConfig{}
}
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, mgr.Stop(ctx))
})
// Set the time to a known value.
mClock := quartz.NewMock(t)
mClock.Set(time.Date(2024, 1, 15, 9, 0, 0, 0, time.UTC))
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), mClock)
require.NoError(t, err)
user := createSampleUser(t, store)
// When: A notification is enqueued, it enqueues the correct amount of notifications.
enqueued, err := enq.Enqueue(ctx, user.ID, notifications.TemplateWorkspaceDeleted,
map[string]string{"initiator": "danny"}, "test", user.ID)
require.NoError(t, err)
require.Len(t, enqueued, tt.expectedEnqueued)
})
}
}
type fakeHandler struct {
mu sync.RWMutex
succeeded, failed []string

View File

@ -2,6 +2,7 @@ package notifications_test
import (
"context"
"net/url"
"sync/atomic"
"testing"
"text/template"
@ -21,6 +22,18 @@ import (
)
func defaultNotificationsConfig(method database.NotificationMethod) codersdk.NotificationsConfig {
var (
smtp codersdk.NotificationsEmailConfig
webhook codersdk.NotificationsWebhookConfig
)
switch method {
case database.NotificationMethodSmtp:
smtp.Smarthost = serpent.String("localhost:1337")
case database.NotificationMethodWebhook:
webhook.Endpoint = serpent.URL(url.URL{Host: "localhost"})
}
return codersdk.NotificationsConfig{
Method: serpent.String(method),
MaxSendAttempts: 5,
@ -31,8 +44,11 @@ func defaultNotificationsConfig(method database.NotificationMethod) codersdk.Not
RetryInterval: serpent.Duration(time.Millisecond * 50),
LeaseCount: 10,
StoreSyncBufferSize: 50,
SMTP: codersdk.NotificationsEmailConfig{},
Webhook: codersdk.NotificationsWebhookConfig{},
SMTP: smtp,
Webhook: webhook,
Inbox: codersdk.NotificationsInboxConfig{
Enabled: serpent.Bool(true),
},
}
}