mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
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:
16
coderd/apidoc/docs.go
generated
16
coderd/apidoc/docs.go
generated
@ -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": {
|
||||
|
16
coderd/apidoc/swagger.json
generated
16
coderd/apidoc/swagger.json
generated
@ -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": {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user