Skip to content

Commit d6302f9

Browse files
committed
Merge branch 'main' into org-members-page
2 parents f234db5 + 687d953 commit d6302f9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1251
-289
lines changed

cli/notifications.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/serpent"
9+
10+
"github.com/coder/coder/v2/codersdk"
11+
)
12+
13+
func (r *RootCmd) notifications() *serpent.Command {
14+
cmd := &serpent.Command{
15+
Use: "notifications",
16+
Short: "Manage Coder notifications",
17+
Long: "Administrators can use these commands to change notification settings.\n" + FormatExamples(
18+
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).",
20+
Command: "coder notifications pause",
21+
},
22+
Example{
23+
Description: "Resume Coder notifications",
24+
Command: "coder notifications resume",
25+
},
26+
),
27+
Aliases: []string{"notification"},
28+
Handler: func(inv *serpent.Invocation) error {
29+
return inv.Command.HelpHandler(inv)
30+
},
31+
Children: []*serpent.Command{
32+
r.pauseNotifications(),
33+
r.resumeNotifications(),
34+
},
35+
}
36+
return cmd
37+
}
38+
39+
func (r *RootCmd) pauseNotifications() *serpent.Command {
40+
client := new(codersdk.Client)
41+
cmd := &serpent.Command{
42+
Use: "pause",
43+
Short: "Pause notifications",
44+
Middleware: serpent.Chain(
45+
serpent.RequireNArgs(0),
46+
r.InitClient(client),
47+
),
48+
Handler: func(inv *serpent.Invocation) error {
49+
err := client.PutNotificationsSettings(inv.Context(), codersdk.NotificationsSettings{
50+
NotifierPaused: true,
51+
})
52+
if err != nil {
53+
return xerrors.Errorf("unable to pause notifications: %w", err)
54+
}
55+
56+
_, _ = fmt.Fprintln(inv.Stderr, "Notifications are now paused.")
57+
return nil
58+
},
59+
}
60+
return cmd
61+
}
62+
63+
func (r *RootCmd) resumeNotifications() *serpent.Command {
64+
client := new(codersdk.Client)
65+
cmd := &serpent.Command{
66+
Use: "resume",
67+
Short: "Resume notifications",
68+
Middleware: serpent.Chain(
69+
serpent.RequireNArgs(0),
70+
r.InitClient(client),
71+
),
72+
Handler: func(inv *serpent.Invocation) error {
73+
err := client.PutNotificationsSettings(inv.Context(), codersdk.NotificationsSettings{
74+
NotifierPaused: false,
75+
})
76+
if err != nil {
77+
return xerrors.Errorf("unable to resume notifications: %w", err)
78+
}
79+
80+
_, _ = fmt.Fprintln(inv.Stderr, "Notifications are now resumed.")
81+
return nil
82+
},
83+
}
84+
return cmd
85+
}

cli/notifications_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"net/http"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/coder/coder/v2/cli/clitest"
14+
"github.com/coder/coder/v2/coderd/coderdtest"
15+
"github.com/coder/coder/v2/codersdk"
16+
"github.com/coder/coder/v2/testutil"
17+
)
18+
19+
func TestNotifications(t *testing.T) {
20+
t.Parallel()
21+
22+
tests := []struct {
23+
name string
24+
command string
25+
expectPaused bool
26+
}{
27+
{
28+
name: "PauseNotifications",
29+
command: "pause",
30+
expectPaused: true,
31+
},
32+
{
33+
name: "ResumeNotifications",
34+
command: "resume",
35+
expectPaused: false,
36+
},
37+
}
38+
39+
for _, tt := range tests {
40+
tt := tt
41+
t.Run(tt.name, func(t *testing.T) {
42+
t.Parallel()
43+
44+
// given
45+
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
46+
_ = coderdtest.CreateFirstUser(t, ownerClient)
47+
48+
// when
49+
inv, root := clitest.New(t, "notifications", tt.command)
50+
clitest.SetupConfig(t, ownerClient, root)
51+
52+
var buf bytes.Buffer
53+
inv.Stdout = &buf
54+
err := inv.Run()
55+
require.NoError(t, err)
56+
57+
// then
58+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
59+
t.Cleanup(cancel)
60+
settingsJSON, err := db.GetNotificationsSettings(ctx)
61+
require.NoError(t, err)
62+
63+
var settings codersdk.NotificationsSettings
64+
err = json.Unmarshal([]byte(settingsJSON), &settings)
65+
require.NoError(t, err)
66+
require.Equal(t, tt.expectPaused, settings.NotifierPaused)
67+
})
68+
}
69+
}
70+
71+
func TestPauseNotifications_RegularUser(t *testing.T) {
72+
t.Parallel()
73+
74+
// given
75+
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
76+
owner := coderdtest.CreateFirstUser(t, ownerClient)
77+
anotherClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
78+
79+
// when
80+
inv, root := clitest.New(t, "notifications", "pause")
81+
clitest.SetupConfig(t, anotherClient, root)
82+
83+
var buf bytes.Buffer
84+
inv.Stdout = &buf
85+
err := inv.Run()
86+
var sdkError *codersdk.Error
87+
require.Error(t, err)
88+
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
89+
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
90+
assert.Contains(t, sdkError.Message, "Insufficient permissions to update notifications settings.")
91+
92+
// then
93+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
94+
t.Cleanup(cancel)
95+
settingsJSON, err := db.GetNotificationsSettings(ctx)
96+
require.NoError(t, err)
97+
98+
var settings codersdk.NotificationsSettings
99+
err = json.Unmarshal([]byte(settingsJSON), &settings)
100+
require.NoError(t, err)
101+
require.False(t, settings.NotifierPaused) // still running
102+
}

cli/root.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
8787
r.login(),
8888
r.logout(),
8989
r.netcheck(),
90+
r.notifications(),
91+
r.organizations(),
9092
r.portForward(),
9193
r.publickey(),
9294
r.resetPassword(),
@@ -95,7 +97,6 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
9597
r.tokens(),
9698
r.users(),
9799
r.version(defaultVersionInfo),
98-
r.organizations(),
99100

100101
// Workspace Commands
101102
r.autoupdate(),
@@ -120,11 +121,11 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
120121
r.whoami(),
121122

122123
// Hidden
124+
r.expCmd(),
123125
r.gitssh(),
126+
r.support(),
124127
r.vscodeSSH(),
125128
r.workspaceAgent(),
126-
r.expCmd(),
127-
r.support(),
128129
}
129130
}
130131

cli/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
983983
)
984984
if experiments.Enabled(codersdk.ExperimentNotifications) {
985985
cfg := options.DeploymentValues.Notifications
986+
metrics := notifications.NewMetrics(options.PrometheusRegistry)
986987

987988
// The enqueuer is responsible for enqueueing notifications to the given store.
988989
enqueuer, err := notifications.NewStoreEnqueuer(cfg, options.Database, templateHelpers(options), logger.Named("notifications.enqueuer"))
@@ -994,7 +995,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
994995
// The notification manager is responsible for:
995996
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
996997
// - keeping the store updated with status updates
997-
notificationsManager, err = notifications.NewManager(cfg, options.Database, logger.Named("notifications.manager"))
998+
notificationsManager, err = notifications.NewManager(cfg, options.Database, metrics, logger.Named("notifications.manager"))
998999
if err != nil {
9991000
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
10001001
}

cli/testdata/coder_--help.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ SUBCOMMANDS:
2727
login Authenticate with Coder deployment
2828
logout Unauthenticate your local session
2929
netcheck Print network debug information for DERP and STUN
30+
notifications Manage Coder notifications
3031
open Open a workspace
3132
ping Ping a workspace
3233
port-forward Forward ports from a workspace to the local machine. For
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder notifications
5+
6+
Manage Coder notifications
7+
8+
Aliases: notification
9+
10+
Administrators can use these commands to change notification settings.
11+
- Pause Coder notifications. Administrators can temporarily stop notifiers
12+
from
13+
dispatching messages in case of the target outage (for example: unavailable
14+
SMTP
15+
server or Webhook not responding).:
16+
17+
$ coder notifications pause
18+
19+
- Resume Coder notifications:
20+
21+
$ coder notifications resume
22+
23+
SUBCOMMANDS:
24+
pause Pause notifications
25+
resume Resume notifications
26+
27+
———
28+
Run `coder --help` for a list of global options.
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 pause
5+
6+
Pause notifications
7+
8+
———
9+
Run `coder --help` for a list of global options.
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 resume
5+
6+
Resume notifications
7+
8+
———
9+
Run `coder --help` for a list of global options.

coderd/database/dbauthz/dbauthz.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,9 +1143,9 @@ func (q *querier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context,
11431143
return q.db.DeleteWorkspaceAgentPortSharesByTemplate(ctx, templateID)
11441144
}
11451145

1146-
func (q *querier) EnqueueNotificationMessage(ctx context.Context, arg database.EnqueueNotificationMessageParams) (database.NotificationMessage, error) {
1146+
func (q *querier) EnqueueNotificationMessage(ctx context.Context, arg database.EnqueueNotificationMessageParams) error {
11471147
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
1148-
return database.NotificationMessage{}, err
1148+
return err
11491149
}
11501150
return q.db.EnqueueNotificationMessage(ctx, arg)
11511151
}

coderd/database/dbmem/dbmem.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -935,12 +935,17 @@ func (q *FakeQuerier) AcquireNotificationMessages(_ context.Context, arg databas
935935
q.mutex.Lock()
936936
defer q.mutex.Unlock()
937937

938-
var out []database.AcquireNotificationMessagesRow
939-
for _, nm := range q.notificationMessages {
940-
if len(out) >= int(arg.Count) {
941-
break
942-
}
938+
// Shift the first "Count" notifications off the slice (FIFO).
939+
sz := len(q.notificationMessages)
940+
if sz > int(arg.Count) {
941+
sz = int(arg.Count)
942+
}
943943

944+
list := q.notificationMessages[:sz]
945+
q.notificationMessages = q.notificationMessages[sz:]
946+
947+
var out []database.AcquireNotificationMessagesRow
948+
for _, nm := range list {
944949
acquirableStatuses := []database.NotificationMessageStatus{database.NotificationMessageStatusPending, database.NotificationMessageStatusTemporaryFailure}
945950
if !slices.Contains(acquirableStatuses, nm.Status) {
946951
continue
@@ -956,9 +961,9 @@ func (q *FakeQuerier) AcquireNotificationMessages(_ context.Context, arg databas
956961
ID: nm.ID,
957962
Payload: nm.Payload,
958963
Method: nm.Method,
959-
CreatedBy: nm.CreatedBy,
960964
TitleTemplate: "This is a title with {{.Labels.variable}}",
961965
BodyTemplate: "This is a body with {{.Labels.variable}}",
966+
TemplateID: nm.NotificationTemplateID,
962967
})
963968
}
964969

@@ -1815,10 +1820,10 @@ func (q *FakeQuerier) DeleteWorkspaceAgentPortSharesByTemplate(_ context.Context
18151820
return nil
18161821
}
18171822

1818-
func (q *FakeQuerier) EnqueueNotificationMessage(_ context.Context, arg database.EnqueueNotificationMessageParams) (database.NotificationMessage, error) {
1823+
func (q *FakeQuerier) EnqueueNotificationMessage(_ context.Context, arg database.EnqueueNotificationMessageParams) error {
18191824
err := validateDatabaseType(arg)
18201825
if err != nil {
1821-
return database.NotificationMessage{}, err
1826+
return err
18221827
}
18231828

18241829
q.mutex.Lock()
@@ -1827,7 +1832,7 @@ func (q *FakeQuerier) EnqueueNotificationMessage(_ context.Context, arg database
18271832
var payload types.MessagePayload
18281833
err = json.Unmarshal(arg.Payload, &payload)
18291834
if err != nil {
1830-
return database.NotificationMessage{}, err
1835+
return err
18311836
}
18321837

18331838
nm := database.NotificationMessage{
@@ -1845,7 +1850,7 @@ func (q *FakeQuerier) EnqueueNotificationMessage(_ context.Context, arg database
18451850

18461851
q.notificationMessages = append(q.notificationMessages, nm)
18471852

1848-
return nm, err
1853+
return err
18491854
}
18501855

18511856
func (q *FakeQuerier) FavoriteWorkspace(_ context.Context, arg uuid.UUID) error {

coderd/database/dbmetrics/dbmetrics.go

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

0 commit comments

Comments
 (0)