Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eec6c8c

Browse files
authored
feat: support custom notifications (#19751)
## Description Adds support for sending an ad‑hoc custom notification to the authenticated user via API and CLI. This is useful for surfacing the result of scripts or long‑running tasks. Notifications are delivered through the configured method and the dashboard Inbox, respecting existing preferences and delivery settings. ## Changes * New notification template: “Custom Notification” with a label for a custom title and a custom message. * New API endpoint: `POST /api/v2/notifications/custom` to send a custom notification to the requesting user. * New API endpoint: `GET /notifications/templates/custom` to get custom notification template. * New CLI subcommand: `coder notifications custom <title> <message>` to send a custom notification to the requesting user. * Documentation updates: Add a “Custom notifications” section under Administration > Monitoring > Notifications, including instructions on sending custom notifications and examples of when to use them. Closes: #19611
1 parent 4c98dec commit eec6c8c

26 files changed

+1056
-30
lines changed

cli/notifications.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ func (r *RootCmd) notifications() *serpent.Command {
1616
Short: "Manage Coder notifications",
1717
Long: "Administrators can use these commands to change notification settings.\n" + FormatExamples(
1818
Example{
19-
Description: "Pause Coder notifications. Administrators can temporarily stop notifiers from dispatching messages in case of the target outage (for example: unavailable SMTP server or Webhook not responding).",
19+
Description: "Pause Coder notifications. Administrators can temporarily stop notifiers from dispatching messages in case of the target outage (for example: unavailable SMTP server or Webhook not responding)",
2020
Command: "coder notifications pause",
2121
},
2222
Example{
2323
Description: "Resume Coder notifications",
2424
Command: "coder notifications resume",
2525
},
2626
Example{
27-
Description: "Send a test notification. Administrators can use this to verify the notification target settings.",
27+
Description: "Send a test notification. Administrators can use this to verify the notification target settings",
2828
Command: "coder notifications test",
2929
},
30+
Example{
31+
Description: "Send a custom notification to the requesting user. Sending notifications targeting other users or groups is currently not supported",
32+
Command: "coder notifications custom \"Custom Title\" \"Custom Message\"",
33+
},
3034
),
3135
Aliases: []string{"notification"},
3236
Handler: func(inv *serpent.Invocation) error {
@@ -36,6 +40,7 @@ func (r *RootCmd) notifications() *serpent.Command {
3640
r.pauseNotifications(),
3741
r.resumeNotifications(),
3842
r.testNotifications(),
43+
r.customNotifications(),
3944
},
4045
}
4146
return cmd
@@ -109,3 +114,30 @@ func (r *RootCmd) testNotifications() *serpent.Command {
109114
}
110115
return cmd
111116
}
117+
118+
func (r *RootCmd) customNotifications() *serpent.Command {
119+
client := new(codersdk.Client)
120+
cmd := &serpent.Command{
121+
Use: "custom <title> <message>",
122+
Short: "Send a custom notification",
123+
Middleware: serpent.Chain(
124+
serpent.RequireNArgs(2),
125+
r.InitClient(client),
126+
),
127+
Handler: func(inv *serpent.Invocation) error {
128+
err := client.PostCustomNotification(inv.Context(), codersdk.CustomNotificationRequest{
129+
Content: &codersdk.CustomNotificationContent{
130+
Title: inv.Args[0],
131+
Message: inv.Args[1],
132+
},
133+
})
134+
if err != nil {
135+
return xerrors.Errorf("unable to post custom notification: %w", err)
136+
}
137+
138+
_, _ = fmt.Fprintln(inv.Stderr, "A custom notification has been sent.")
139+
return nil
140+
},
141+
}
142+
return cmd
143+
}

cli/notifications_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212

1313
"github.com/coder/coder/v2/cli/clitest"
1414
"github.com/coder/coder/v2/coderd/coderdtest"
15+
"github.com/coder/coder/v2/coderd/database"
16+
"github.com/coder/coder/v2/coderd/database/dbgen"
1517
"github.com/coder/coder/v2/coderd/notifications"
1618
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
1719
"github.com/coder/coder/v2/codersdk"
@@ -166,3 +168,102 @@ func TestNotificationsTest(t *testing.T) {
166168
require.Len(t, sent, 0)
167169
})
168170
}
171+
172+
func TestCustomNotifications(t *testing.T) {
173+
t.Parallel()
174+
175+
t.Run("BadRequest", func(t *testing.T) {
176+
t.Parallel()
177+
178+
notifyEnq := &notificationstest.FakeEnqueuer{}
179+
180+
ownerClient := coderdtest.New(t, &coderdtest.Options{
181+
DeploymentValues: coderdtest.DeploymentValues(t),
182+
NotificationsEnqueuer: notifyEnq,
183+
})
184+
185+
// Given: A member user
186+
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
187+
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
188+
189+
// When: The member user attempts to send a custom notification with empty title and message
190+
inv, root := clitest.New(t, "notifications", "custom", "", "")
191+
clitest.SetupConfig(t, memberClient, root)
192+
193+
// Then: an error is expected with no notifications sent
194+
err := inv.Run()
195+
var sdkError *codersdk.Error
196+
require.Error(t, err)
197+
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
198+
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
199+
require.Equal(t, "Invalid request body", sdkError.Message)
200+
201+
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
202+
require.Len(t, sent, 0)
203+
})
204+
205+
t.Run("SystemUserNotAllowed", func(t *testing.T) {
206+
t.Parallel()
207+
208+
notifyEnq := &notificationstest.FakeEnqueuer{}
209+
210+
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
211+
DeploymentValues: coderdtest.DeploymentValues(t),
212+
NotificationsEnqueuer: notifyEnq,
213+
})
214+
215+
// Given: A system user (prebuilds system user)
216+
_, token := dbgen.APIKey(t, db, database.APIKey{
217+
UserID: database.PrebuildsSystemUserID,
218+
LoginType: database.LoginTypeNone,
219+
})
220+
systemUserClient := codersdk.New(ownerClient.URL)
221+
systemUserClient.SetSessionToken(token)
222+
223+
// When: The system user attempts to send a custom notification
224+
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
225+
clitest.SetupConfig(t, systemUserClient, root)
226+
227+
// Then: an error is expected with no notifications sent
228+
err := inv.Run()
229+
var sdkError *codersdk.Error
230+
require.Error(t, err)
231+
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
232+
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
233+
require.Equal(t, "Forbidden", sdkError.Message)
234+
235+
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
236+
require.Len(t, sent, 0)
237+
})
238+
239+
t.Run("Success", func(t *testing.T) {
240+
t.Parallel()
241+
242+
notifyEnq := &notificationstest.FakeEnqueuer{}
243+
244+
ownerClient := coderdtest.New(t, &coderdtest.Options{
245+
DeploymentValues: coderdtest.DeploymentValues(t),
246+
NotificationsEnqueuer: notifyEnq,
247+
})
248+
249+
// Given: A member user
250+
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
251+
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
252+
253+
// When: The member user attempts to send a custom notification
254+
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
255+
clitest.SetupConfig(t, memberClient, root)
256+
257+
// Then: we expect a custom notification to be sent to the member user
258+
err := inv.Run()
259+
require.NoError(t, err)
260+
261+
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateCustomNotification))
262+
require.Len(t, sent, 1)
263+
require.Equal(t, memberUser.ID, sent[0].UserID)
264+
require.Len(t, sent[0].Labels, 2)
265+
require.Equal(t, "Custom Title", sent[0].Labels["custom_title"])
266+
require.Equal(t, "Custom Message", sent[0].Labels["custom_message"])
267+
require.Equal(t, memberUser.ID.String(), sent[0].CreatedBy)
268+
})
269+
}

cli/testdata/coder_notifications_--help.golden

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ USAGE:
1212
from
1313
dispatching messages in case of the target outage (for example: unavailable
1414
SMTP
15-
server or Webhook not responding).:
15+
server or Webhook not responding):
1616

1717
$ coder notifications pause
1818

@@ -22,11 +22,17 @@ USAGE:
2222

2323
- Send a test notification. Administrators can use this to verify the
2424
notification
25-
target settings.:
25+
target settings:
2626

2727
$ coder notifications test
28+
29+
- Send a custom notification to the requesting user. Sending notifications
30+
targeting other users or groups is currently not supported:
31+
32+
$ coder notifications custom "Custom Title" "Custom Message"
2833

2934
SUBCOMMANDS:
35+
custom Send a custom notification
3036
pause Pause notifications
3137
resume Resume notifications
3238
test Send a test notification
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder notifications custom <title> <message>
5+
6+
Send a custom notification
7+
8+
———
9+
Run `coder --help` for a list of global options.

coderd/apidoc/docs.go

Lines changed: 113 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)