mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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: https://github.com/coder/coder/issues/19611
This commit is contained in:
+34
-2
@@ -16,7 +16,7 @@ func (r *RootCmd) notifications() *serpent.Command {
|
||||
Short: "Manage Coder notifications",
|
||||
Long: "Administrators can use these commands to change notification settings.\n" + FormatExamples(
|
||||
Example{
|
||||
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).",
|
||||
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)",
|
||||
Command: "coder notifications pause",
|
||||
},
|
||||
Example{
|
||||
@@ -24,9 +24,13 @@ func (r *RootCmd) notifications() *serpent.Command {
|
||||
Command: "coder notifications resume",
|
||||
},
|
||||
Example{
|
||||
Description: "Send a test notification. Administrators can use this to verify the notification target settings.",
|
||||
Description: "Send a test notification. Administrators can use this to verify the notification target settings",
|
||||
Command: "coder notifications test",
|
||||
},
|
||||
Example{
|
||||
Description: "Send a custom notification to the requesting user. Sending notifications targeting other users or groups is currently not supported",
|
||||
Command: "coder notifications custom \"Custom Title\" \"Custom Message\"",
|
||||
},
|
||||
),
|
||||
Aliases: []string{"notification"},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
@@ -36,6 +40,7 @@ func (r *RootCmd) notifications() *serpent.Command {
|
||||
r.pauseNotifications(),
|
||||
r.resumeNotifications(),
|
||||
r.testNotifications(),
|
||||
r.customNotifications(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
@@ -109,3 +114,30 @@ func (r *RootCmd) testNotifications() *serpent.Command {
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) customNotifications() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
cmd := &serpent.Command{
|
||||
Use: "custom <title> <message>",
|
||||
Short: "Send a custom notification",
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireNArgs(2),
|
||||
r.InitClient(client),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
err := client.PostCustomNotification(inv.Context(), codersdk.CustomNotificationRequest{
|
||||
Content: &codersdk.CustomNotificationContent{
|
||||
Title: inv.Args[0],
|
||||
Message: inv.Args[1],
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("unable to post custom notification: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "A custom notification has been sent.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -166,3 +168,102 @@ func TestNotificationsTest(t *testing.T) {
|
||||
require.Len(t, sent, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCustomNotifications(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("BadRequest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||
NotificationsEnqueuer: notifyEnq,
|
||||
})
|
||||
|
||||
// Given: A member user
|
||||
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
||||
|
||||
// When: The member user attempts to send a custom notification with empty title and message
|
||||
inv, root := clitest.New(t, "notifications", "custom", "", "")
|
||||
clitest.SetupConfig(t, memberClient, root)
|
||||
|
||||
// Then: an error is expected with no notifications sent
|
||||
err := inv.Run()
|
||||
var sdkError *codersdk.Error
|
||||
require.Error(t, err)
|
||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
|
||||
require.Equal(t, "Invalid request body", sdkError.Message)
|
||||
|
||||
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
||||
require.Len(t, sent, 0)
|
||||
})
|
||||
|
||||
t.Run("SystemUserNotAllowed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||
NotificationsEnqueuer: notifyEnq,
|
||||
})
|
||||
|
||||
// Given: A system user (prebuilds system user)
|
||||
_, token := dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: database.PrebuildsSystemUserID,
|
||||
LoginType: database.LoginTypeNone,
|
||||
})
|
||||
systemUserClient := codersdk.New(ownerClient.URL)
|
||||
systemUserClient.SetSessionToken(token)
|
||||
|
||||
// When: The system user attempts to send a custom notification
|
||||
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
|
||||
clitest.SetupConfig(t, systemUserClient, root)
|
||||
|
||||
// Then: an error is expected with no notifications sent
|
||||
err := inv.Run()
|
||||
var sdkError *codersdk.Error
|
||||
require.Error(t, err)
|
||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||
require.Equal(t, "Forbidden", sdkError.Message)
|
||||
|
||||
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
||||
require.Len(t, sent, 0)
|
||||
})
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||
NotificationsEnqueuer: notifyEnq,
|
||||
})
|
||||
|
||||
// Given: A member user
|
||||
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
||||
|
||||
// When: The member user attempts to send a custom notification
|
||||
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
|
||||
clitest.SetupConfig(t, memberClient, root)
|
||||
|
||||
// Then: we expect a custom notification to be sent to the member user
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateCustomNotification))
|
||||
require.Len(t, sent, 1)
|
||||
require.Equal(t, memberUser.ID, sent[0].UserID)
|
||||
require.Len(t, sent[0].Labels, 2)
|
||||
require.Equal(t, "Custom Title", sent[0].Labels["custom_title"])
|
||||
require.Equal(t, "Custom Message", sent[0].Labels["custom_message"])
|
||||
require.Equal(t, memberUser.ID.String(), sent[0].CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
+8
-2
@@ -12,7 +12,7 @@ USAGE:
|
||||
from
|
||||
dispatching messages in case of the target outage (for example: unavailable
|
||||
SMTP
|
||||
server or Webhook not responding).:
|
||||
server or Webhook not responding):
|
||||
|
||||
$ coder notifications pause
|
||||
|
||||
@@ -22,11 +22,17 @@ USAGE:
|
||||
|
||||
- Send a test notification. Administrators can use this to verify the
|
||||
notification
|
||||
target settings.:
|
||||
target settings:
|
||||
|
||||
$ coder notifications test
|
||||
|
||||
- Send a custom notification to the requesting user. Sending notifications
|
||||
targeting other users or groups is currently not supported:
|
||||
|
||||
$ coder notifications custom "Custom Title" "Custom Message"
|
||||
|
||||
SUBCOMMANDS:
|
||||
custom Send a custom notification
|
||||
pause Pause notifications
|
||||
resume Resume notifications
|
||||
test Send a test notification
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder notifications custom <title> <message>
|
||||
|
||||
Send a custom notification
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
Reference in New Issue
Block a user