Skip to content

Commit 377eaab

Browse files
committed
add cli command to regenerate vapid keypair and remove existing subscriptions
1 parent 84e3ace commit 377eaab

25 files changed

+779
-99
lines changed

cli/server.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -776,11 +776,18 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
776776
return xerrors.Errorf("set deployment id: %w", err)
777777
}
778778

779-
pushNotifier, err := push.New(ctx, &options.Logger, options.Database)
780-
if err != nil {
781-
return xerrors.Errorf("failed to create push notifier: %w", err)
779+
// Manage push notifications.
780+
{
781+
pushNotifier, err := push.New(ctx, &options.Logger, options.Database)
782+
if err != nil {
783+
options.Logger.Error(ctx, "failed to create push notifier", slog.Error(err))
784+
options.Logger.Warn(ctx, "push notifications will not work until the VAPID keys are regenerated")
785+
pushNotifier = &push.NoopNotifier{
786+
Msg: "Push notifications are disabled due to a system error. Please contact your Coder administrator.",
787+
}
788+
}
789+
options.PushNotifier = pushNotifier
782790
}
783-
options.PushNotifier = pushNotifier
784791

785792
githubOAuth2ConfigParams, err := getGithubOAuth2ConfigParams(ctx, options.Database, vals)
786793
if err != nil {
@@ -1262,6 +1269,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
12621269
}
12631270

12641271
createAdminUserCmd := r.newCreateAdminUserCommand()
1272+
regenerateVapidKeypairCmd := r.newRegenerateVapidKeypairCommand()
12651273

12661274
rawURLOpt := serpent.Option{
12671275
Flag: "raw-url",
@@ -1275,7 +1283,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
12751283

12761284
serverCmd.Children = append(
12771285
serverCmd.Children,
1278-
createAdminUserCmd, postgresBuiltinURLCmd, postgresBuiltinServeCmd,
1286+
createAdminUserCmd, postgresBuiltinURLCmd, postgresBuiltinServeCmd, regenerateVapidKeypairCmd,
12791287
)
12801288

12811289
return serverCmd
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//go:build !slim
2+
3+
package cli
4+
5+
import (
6+
"fmt"
7+
8+
"golang.org/x/xerrors"
9+
10+
"cdr.dev/slog"
11+
"cdr.dev/slog/sloggers/sloghuman"
12+
13+
"github.com/coder/coder/v2/cli/cliui"
14+
"github.com/coder/coder/v2/coderd/database"
15+
"github.com/coder/coder/v2/coderd/database/awsiamrds"
16+
"github.com/coder/coder/v2/coderd/notifications/push"
17+
"github.com/coder/coder/v2/codersdk"
18+
"github.com/coder/serpent"
19+
)
20+
21+
func (r *RootCmd) newRegenerateVapidKeypairCommand() *serpent.Command {
22+
var (
23+
regenVapidKeypairDBURL string
24+
regenVapidKeypairPgAuth string
25+
)
26+
regenerateVapidKeypairCommand := &serpent.Command{
27+
Use: "regenerate-vapid-keypair",
28+
Short: "Regenerate the VAPID keypair used for push notifications.",
29+
Handler: func(inv *serpent.Invocation) error {
30+
var (
31+
ctx, cancel = inv.SignalNotifyContext(inv.Context(), StopSignals...)
32+
cfg = r.createConfig()
33+
logger = inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr))
34+
)
35+
if r.verbose {
36+
logger = logger.Leveled(slog.LevelDebug)
37+
}
38+
39+
defer cancel()
40+
41+
if regenVapidKeypairDBURL == "" {
42+
cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", cfg.PostgresPath())
43+
url, closePg, err := startBuiltinPostgres(ctx, cfg, logger, "")
44+
if err != nil {
45+
return err
46+
}
47+
defer func() {
48+
_ = closePg()
49+
}()
50+
regenVapidKeypairDBURL = url
51+
}
52+
53+
sqlDriver := "postgres"
54+
var err error
55+
if codersdk.PostgresAuth(regenVapidKeypairPgAuth) == codersdk.PostgresAuthAWSIAMRDS {
56+
sqlDriver, err = awsiamrds.Register(inv.Context(), sqlDriver)
57+
if err != nil {
58+
return xerrors.Errorf("register aws rds iam auth: %w", err)
59+
}
60+
}
61+
62+
sqlDB, err := ConnectToPostgres(ctx, logger, sqlDriver, regenVapidKeypairDBURL, nil)
63+
if err != nil {
64+
return xerrors.Errorf("connect to postgres: %w", err)
65+
}
66+
defer func() {
67+
_ = sqlDB.Close()
68+
}()
69+
db := database.New(sqlDB)
70+
71+
// Confirm that the user really wants to regenerate the VAPID keypair.
72+
cliui.Infof(inv.Stdout, "Regenerating VAPID keypair...")
73+
cliui.Infof(inv.Stdout, "This will delete all existing push notification subscriptions.")
74+
cliui.Infof(inv.Stdout, "Are you sure you want to continue? (y/N)")
75+
76+
if resp, err := cliui.Prompt(inv, cliui.PromptOptions{
77+
IsConfirm: true,
78+
Default: cliui.ConfirmNo,
79+
}); err != nil || resp != cliui.ConfirmYes {
80+
return xerrors.Errorf("VAPID keypair regeneration failed: %w", err)
81+
}
82+
83+
if _, _, err := push.RegenerateVAPIDKeys(ctx, db); err != nil {
84+
return xerrors.Errorf("regenerate vapid keypair: %w", err)
85+
}
86+
87+
_, _ = fmt.Fprintln(inv.Stdout, "VAPID keypair regenerated successfully.")
88+
return nil
89+
},
90+
}
91+
92+
regenerateVapidKeypairCommand.Options.Add(
93+
cliui.SkipPromptOption(),
94+
serpent.Option{
95+
Env: "CODER_PG_CONNECTION_URL",
96+
Flag: "postgres-url",
97+
Description: "URL of a PostgreSQL database. If empty, the built-in PostgreSQL deployment will be used (Coder must not be already running in this case).",
98+
Value: serpent.StringOf(&regenVapidKeypairDBURL),
99+
},
100+
serpent.Option{
101+
Name: "Postgres Connection Auth",
102+
Description: "Type of auth to use when connecting to postgres.",
103+
Flag: "postgres-connection-auth",
104+
Env: "CODER_PG_CONNECTION_AUTH",
105+
Default: "password",
106+
Value: serpent.EnumOf(&regenVapidKeypairPgAuth, codersdk.PostgresAuthDrivers...),
107+
},
108+
)
109+
110+
return regenerateVapidKeypairCommand
111+
}
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/cli/clitest"
11+
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/database/dbgen"
13+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
14+
"github.com/coder/coder/v2/pty/ptytest"
15+
"github.com/coder/coder/v2/testutil"
16+
)
17+
18+
func TestRegenerateVapidKeypair(t *testing.T) {
19+
t.Parallel()
20+
21+
t.Run("NoExistingVAPIDKeys", func(t *testing.T) {
22+
t.Parallel()
23+
24+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
25+
t.Cleanup(cancel)
26+
27+
connectionURL, err := dbtestutil.Open(t)
28+
require.NoError(t, err)
29+
30+
sqlDB, err := sql.Open("postgres", connectionURL)
31+
require.NoError(t, err)
32+
defer sqlDB.Close()
33+
34+
db := database.New(sqlDB)
35+
// Ensure there is no existing VAPID keypair.
36+
rows, err := db.GetNotificationVAPIDKeys(ctx)
37+
require.NoError(t, err)
38+
require.Empty(t, rows)
39+
40+
inv, _ := clitest.New(t, "server", "regenerate-vapid-keypair", "--postgres-url", connectionURL, "--yes")
41+
42+
pty := ptytest.New(t)
43+
inv.Stdout = pty.Output()
44+
inv.Stderr = pty.Output()
45+
clitest.Start(t, inv)
46+
47+
pty.ExpectMatchContext(ctx, "Regenerating VAPID keypair...")
48+
pty.ExpectMatchContext(ctx, "This will delete all existing push notification subscriptions.")
49+
pty.ExpectMatchContext(ctx, "Are you sure you want to continue? (y/N)")
50+
pty.WriteLine("y")
51+
pty.ExpectMatchContext(ctx, "VAPID keypair regenerated successfully.")
52+
53+
// Ensure the VAPID keypair was created.
54+
keys, err := db.GetNotificationVAPIDKeys(ctx)
55+
require.NoError(t, err)
56+
require.NotEmpty(t, keys.VapidPublicKey)
57+
require.NotEmpty(t, keys.VapidPrivateKey)
58+
})
59+
60+
t.Run("ExistingVAPIDKeys", func(t *testing.T) {
61+
t.Parallel()
62+
63+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
64+
t.Cleanup(cancel)
65+
66+
connectionURL, err := dbtestutil.Open(t)
67+
require.NoError(t, err)
68+
69+
sqlDB, err := sql.Open("postgres", connectionURL)
70+
require.NoError(t, err)
71+
defer sqlDB.Close()
72+
73+
db := database.New(sqlDB)
74+
for i := 0; i < 10; i++ {
75+
// Insert a few fake users.
76+
u := dbgen.User(t, db, database.User{})
77+
// Insert a few fake push subscriptions for each user.
78+
for j := 0; j < 10; j++ {
79+
_ = dbgen.NotificationPushSubscription(t, db, database.InsertNotificationPushSubscriptionParams{
80+
UserID: u.ID,
81+
})
82+
}
83+
}
84+
85+
inv, _ := clitest.New(t, "server", "regenerate-vapid-keypair", "--postgres-url", connectionURL, "--yes")
86+
87+
pty := ptytest.New(t)
88+
inv.Stdout = pty.Output()
89+
inv.Stderr = pty.Output()
90+
clitest.Start(t, inv)
91+
92+
pty.ExpectMatchContext(ctx, "Regenerating VAPID keypair...")
93+
pty.ExpectMatchContext(ctx, "This will delete all existing push notification subscriptions.")
94+
pty.ExpectMatchContext(ctx, "Are you sure you want to continue? (y/N)")
95+
pty.WriteLine("y")
96+
pty.ExpectMatchContext(ctx, "VAPID keypair regenerated successfully.")
97+
98+
// Ensure the VAPID keypair was created.
99+
keys, err := db.GetNotificationVAPIDKeys(ctx)
100+
require.NoError(t, err)
101+
require.NotEmpty(t, keys.VapidPublicKey)
102+
require.NotEmpty(t, keys.VapidPrivateKey)
103+
104+
// Ensure the push subscriptions were deleted.
105+
var count int64
106+
rows, err := sqlDB.QueryContext(ctx, "SELECT COUNT(*) FROM notification_push_subscriptions")
107+
require.NoError(t, err)
108+
t.Cleanup(func() {
109+
_ = rows.Close()
110+
})
111+
require.True(t, rows.Next())
112+
require.NoError(t, rows.Scan(&count))
113+
require.Equal(t, int64(0), count)
114+
})
115+
}

cli/testdata/coder_server_--help.golden

+8-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ USAGE:
66
Start a Coder server
77

88
SUBCOMMANDS:
9-
create-admin-user Create a new admin user with the given username,
10-
email and password and adds it to every
11-
organization.
12-
postgres-builtin-serve Run the built-in PostgreSQL deployment.
13-
postgres-builtin-url Output the connection URL for the built-in
14-
PostgreSQL deployment.
9+
create-admin-user Create a new admin user with the given username,
10+
email and password and adds it to every
11+
organization.
12+
postgres-builtin-serve Run the built-in PostgreSQL deployment.
13+
postgres-builtin-url Output the connection URL for the built-in
14+
PostgreSQL deployment.
15+
regenerate-vapid-keypair Regenerate the VAPID keypair used for push
16+
notifications.
1517

1618
OPTIONS:
1719
--allow-workspace-renames bool, $CODER_ALLOW_WORKSPACE_RENAMES (default: false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder server regenerate-vapid-keypair [flags]
5+
6+
Regenerate the VAPID keypair used for push notifications.
7+
8+
OPTIONS:
9+
--postgres-connection-auth password|awsiamrds, $CODER_PG_CONNECTION_AUTH (default: password)
10+
Type of auth to use when connecting to postgres.
11+
12+
--postgres-url string, $CODER_PG_CONNECTION_URL
13+
URL of a PostgreSQL database. If empty, the built-in PostgreSQL
14+
deployment will be used (Coder must not be already running in this
15+
case).
16+
17+
-y, --yes bool
18+
Bypass prompts.
19+
20+
———
21+
Run `coder --help` for a list of global options.

coderd/apidoc/docs.go

+78
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)