Skip to content

Commit 6f42c78

Browse files
authored
Merge branch 'main' into dependabot/github_actions/github-actions-8ec757eab7
2 parents 45e8fb9 + 978364b commit 6f42c78

Some content is hidden

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

63 files changed

+4795
-873
lines changed

.github/actions/setup-go/action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: |
44
inputs:
55
version:
66
description: "The Go version to use."
7-
default: "1.22.4"
7+
default: "1.22.5"
88
runs:
99
using: "composite"
1010
steps:

.github/dependabot.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ updates:
3939
prefix: "chore"
4040
labels: []
4141
open-pull-requests-limit: 15
42+
groups:
43+
x:
44+
patterns:
45+
- "golang.org/x/*"
4246
ignore:
4347
# Ignore patch updates for all dependencies
4448
- dependency-name: "*"
@@ -73,6 +77,32 @@ updates:
7377
commit-message:
7478
prefix: "chore"
7579
labels: []
80+
groups:
81+
xterm:
82+
patterns:
83+
- "@xterm*"
84+
mui:
85+
patterns:
86+
- "@mui*"
87+
react:
88+
patterns:
89+
- "react*"
90+
- "@types/react*"
91+
emotion:
92+
patterns:
93+
- "@emotion*"
94+
eslint:
95+
patterns:
96+
- "eslint*"
97+
- "@typescript-eslint*"
98+
jest:
99+
patterns:
100+
- "jest*"
101+
- "@types/jest"
102+
vite:
103+
patterns:
104+
- "vite*"
105+
- "@vitejs/plugin-react"
76106
ignore:
77107
# Ignore patch updates for all dependencies
78108
- dependency-name: "*"
@@ -83,4 +113,10 @@ updates:
83113
- dependency-name: "@types/node"
84114
update-types:
85115
- version-update:semver-major
116+
# Ignore @storybook updates, run `pnpm dlx storybook@latest upgrade` to upgrade manually
117+
- dependency-name: "*storybook*" # matches @storybook/* and storybook*
118+
update-types:
119+
- version-update:semver-major
120+
- version-update:semver-minor
121+
- version-update:semver-patch
86122
open-pull-requests-limit: 15

.golangci.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ linters-settings:
195195
- name: var-naming
196196
- name: waitgroup-by-value
197197

198+
# irrelevant as of Go v1.22: https://go.dev/blog/loopvar-preview
199+
govet:
200+
disable:
201+
- loopclosure
202+
198203
issues:
199204
# Rules listed here: https://github.com/securego/gosec#available-rules
200205
exclude-rules:

cli/configssh.go

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type sshConfigOptions struct {
5454
disableAutostart bool
5555
header []string
5656
headerCommand string
57+
removedKeys map[string]bool
5758
}
5859

5960
// addOptions expects options in the form of "option=value" or "option value".
@@ -74,30 +75,20 @@ func (o *sshConfigOptions) addOption(option string) error {
7475
if err != nil {
7576
return err
7677
}
77-
for i, existing := range o.sshOptions {
78-
// Override existing option if they share the same key.
79-
// This is case-insensitive. Parsing each time might be a little slow,
80-
// but it is ok.
81-
existingKey, _, err := codersdk.ParseSSHConfigOption(existing)
82-
if err != nil {
83-
// Don't mess with original values if there is an error.
84-
// This could have come from the user's manual edits.
85-
continue
86-
}
87-
if strings.EqualFold(existingKey, key) {
88-
if value == "" {
89-
// Delete existing option.
90-
o.sshOptions = append(o.sshOptions[:i], o.sshOptions[i+1:]...)
91-
} else {
92-
// Override existing option.
93-
o.sshOptions[i] = option
94-
}
95-
return nil
96-
}
78+
lowerKey := strings.ToLower(key)
79+
if o.removedKeys != nil && o.removedKeys[lowerKey] {
80+
// Key marked as removed, skip.
81+
return nil
9782
}
98-
// Only append the option if it is not empty.
83+
// Only append the option if it is not empty
84+
// (we interpret empty as removal).
9985
if value != "" {
10086
o.sshOptions = append(o.sshOptions, option)
87+
} else {
88+
if o.removedKeys == nil {
89+
o.removedKeys = make(map[string]bool)
90+
}
91+
o.removedKeys[lowerKey] = true
10192
}
10293
return nil
10394
}
@@ -440,26 +431,29 @@ func (r *RootCmd) configSSH() *serpent.Command {
440431
configOptions := sshConfigOpts
441432
configOptions.sshOptions = nil
442433

443-
// Add standard options.
444-
err := configOptions.addOptions(defaultOptions...)
445-
if err != nil {
446-
return err
434+
// User options first (SSH only uses the first
435+
// option unless it can be given multiple times)
436+
for _, opt := range sshConfigOpts.sshOptions {
437+
err := configOptions.addOptions(opt)
438+
if err != nil {
439+
return xerrors.Errorf("add flag config option %q: %w", opt, err)
440+
}
447441
}
448442

449-
// Override with deployment options
443+
// Deployment options second, allow them to
444+
// override standard options.
450445
for k, v := range coderdConfig.SSHConfigOptions {
451446
opt := fmt.Sprintf("%s %s", k, v)
452447
err := configOptions.addOptions(opt)
453448
if err != nil {
454449
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
455450
}
456451
}
457-
// Override with flag options
458-
for _, opt := range sshConfigOpts.sshOptions {
459-
err := configOptions.addOptions(opt)
460-
if err != nil {
461-
return xerrors.Errorf("add flag config option %q: %w", opt, err)
462-
}
452+
453+
// Finally, add the standard options.
454+
err := configOptions.addOptions(defaultOptions...)
455+
if err != nil {
456+
return err
463457
}
464458

465459
hostBlock := []string{

cli/configssh_internal_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,32 +272,32 @@ func Test_sshConfigOptions_addOption(t *testing.T) {
272272
},
273273
},
274274
{
275-
Name: "Replace",
275+
Name: "AddTwo",
276276
Start: []string{
277277
"foo bar",
278278
},
279279
Add: []string{"Foo baz"},
280280
Expect: []string{
281+
"foo bar",
281282
"Foo baz",
282283
},
283284
},
284285
{
285-
Name: "AddAndReplace",
286+
Name: "AddAndRemove",
286287
Start: []string{
287-
"a b",
288288
"foo bar",
289289
"buzz bazz",
290290
},
291291
Add: []string{
292292
"b c",
293+
"a ", // Empty value, means remove all following entries that start with "a", i.e. next line.
293294
"A hello",
294295
"hello world",
295296
},
296297
Expect: []string{
297298
"foo bar",
298299
"buzz bazz",
299300
"b c",
300-
"A hello",
301301
"hello world",
302302
},
303303
},

cli/configssh_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestConfigSSH(t *testing.T) {
6565

6666
const hostname = "test-coder."
6767
const expectedKey = "ConnectionAttempts"
68-
const removeKey = "ConnectionTimeout"
68+
const removeKey = "ConnectTimeout"
6969
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
7070
ConfigSSH: codersdk.SSHConfigResponse{
7171
HostnamePrefix: hostname,
@@ -620,6 +620,19 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
620620
regexMatch: `ProxyCommand .* --header-command "printf h1=v1 h2='v2'" ssh`,
621621
},
622622
},
623+
{
624+
name: "Multiple remote forwards",
625+
args: []string{
626+
"--yes",
627+
"--ssh-option", "RemoteForward 2222 192.168.11.1:2222",
628+
"--ssh-option", "RemoteForward 2223 192.168.11.1:2223",
629+
},
630+
wantErr: false,
631+
hasAgent: true,
632+
wantConfig: wantConfig{
633+
regexMatch: "RemoteForward 2222 192.168.11.1:2222.*\n.*RemoteForward 2223 192.168.11.1:2223",
634+
},
635+
},
623636
}
624637
for _, tt := range tests {
625638
tt := tt

cli/server.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ import (
5555

5656
"cdr.dev/slog"
5757
"cdr.dev/slog/sloggers/sloghuman"
58+
"github.com/coder/pretty"
59+
"github.com/coder/retry"
60+
"github.com/coder/serpent"
61+
"github.com/coder/wgtunnel/tunnelsdk"
62+
5863
"github.com/coder/coder/v2/buildinfo"
5964
"github.com/coder/coder/v2/cli/clilog"
6065
"github.com/coder/coder/v2/cli/cliui"
@@ -64,6 +69,7 @@ import (
6469
"github.com/coder/coder/v2/coderd/autobuild"
6570
"github.com/coder/coder/v2/coderd/database"
6671
"github.com/coder/coder/v2/coderd/database/awsiamrds"
72+
"github.com/coder/coder/v2/coderd/database/dbauthz"
6773
"github.com/coder/coder/v2/coderd/database/dbmem"
6874
"github.com/coder/coder/v2/coderd/database/dbmetrics"
6975
"github.com/coder/coder/v2/coderd/database/dbpurge"
@@ -73,6 +79,7 @@ import (
7379
"github.com/coder/coder/v2/coderd/externalauth"
7480
"github.com/coder/coder/v2/coderd/gitsshkey"
7581
"github.com/coder/coder/v2/coderd/httpmw"
82+
"github.com/coder/coder/v2/coderd/notifications"
7683
"github.com/coder/coder/v2/coderd/oauthpki"
7784
"github.com/coder/coder/v2/coderd/prometheusmetrics"
7885
"github.com/coder/coder/v2/coderd/prometheusmetrics/insights"
@@ -97,10 +104,6 @@ import (
97104
"github.com/coder/coder/v2/provisionersdk"
98105
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
99106
"github.com/coder/coder/v2/tailnet"
100-
"github.com/coder/pretty"
101-
"github.com/coder/retry"
102-
"github.com/coder/serpent"
103-
"github.com/coder/wgtunnel/tunnelsdk"
104107
)
105108

106109
func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*coderd.OIDCConfig, error) {
@@ -592,6 +595,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
592595
SSHConfigOptions: configSSHOptions,
593596
},
594597
AllowWorkspaceRenames: vals.AllowWorkspaceRenames.Value(),
598+
NotificationsEnqueuer: notifications.NewNoopEnqueuer(), // Changed further down if notifications enabled.
595599
}
596600
if httpServers.TLSConfig != nil {
597601
options.TLSCertificates = httpServers.TLSConfig.Certificates
@@ -660,6 +664,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
660664
options.OIDCConfig = oc
661665
}
662666

667+
experiments := coderd.ReadExperiments(
668+
options.Logger, options.DeploymentValues.Experiments.Value(),
669+
)
670+
663671
// We'll read from this channel in the select below that tracks shutdown. If it remains
664672
// nil, that case of the select will just never fire, but it's important not to have a
665673
// "bare" read on this channel.
@@ -969,6 +977,32 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
969977
options.WorkspaceUsageTracker = tracker
970978
defer tracker.Close()
971979

980+
// Manage notifications.
981+
var (
982+
notificationsManager *notifications.Manager
983+
)
984+
if experiments.Enabled(codersdk.ExperimentNotifications) {
985+
cfg := options.DeploymentValues.Notifications
986+
987+
// The enqueuer is responsible for enqueueing notifications to the given store.
988+
enqueuer, err := notifications.NewStoreEnqueuer(cfg, options.Database, templateHelpers(options), logger.Named("notifications.enqueuer"))
989+
if err != nil {
990+
return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err)
991+
}
992+
options.NotificationsEnqueuer = enqueuer
993+
994+
// The notification manager is responsible for:
995+
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
996+
// - keeping the store updated with status updates
997+
notificationsManager, err = notifications.NewManager(cfg, options.Database, logger.Named("notifications.manager"))
998+
if err != nil {
999+
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
1000+
}
1001+
1002+
// nolint:gocritic // TODO: create own role.
1003+
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))
1004+
}
1005+
9721006
// Wrap the server in middleware that redirects to the access URL if
9731007
// the request is not to a local IP.
9741008
var handler http.Handler = coderAPI.RootHandler
@@ -1049,10 +1083,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
10491083
case <-stopCtx.Done():
10501084
exitErr = stopCtx.Err()
10511085
waitForProvisionerJobs = true
1052-
_, _ = io.WriteString(inv.Stdout, cliui.Bold("Stop caught, waiting for provisioner jobs to complete and gracefully exiting. Use ctrl+\\ to force quit"))
1086+
_, _ = io.WriteString(inv.Stdout, cliui.Bold("Stop caught, waiting for provisioner jobs to complete and gracefully exiting. Use ctrl+\\ to force quit\n"))
10531087
case <-interruptCtx.Done():
10541088
exitErr = interruptCtx.Err()
1055-
_, _ = io.WriteString(inv.Stdout, cliui.Bold("Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit"))
1089+
_, _ = io.WriteString(inv.Stdout, cliui.Bold("Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit\n"))
10561090
case <-tunnelDone:
10571091
exitErr = xerrors.New("dev tunnel closed unexpectedly")
10581092
case <-pubsubWatchdogTimeout:
@@ -1088,6 +1122,21 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
10881122
// Cancel any remaining in-flight requests.
10891123
shutdownConns()
10901124

1125+
if notificationsManager != nil {
1126+
// Stop the notification manager, which will cause any buffered updates to the store to be flushed.
1127+
// If the Stop() call times out, messages that were sent but not reflected as such in the store will have
1128+
// their leases expire after a period of time and will be re-queued for sending.
1129+
// See CODER_NOTIFICATIONS_LEASE_PERIOD.
1130+
cliui.Info(inv.Stdout, "Shutting down notifications manager..."+"\n")
1131+
err = shutdownWithTimeout(notificationsManager.Stop, 5*time.Second)
1132+
if err != nil {
1133+
cliui.Warnf(inv.Stderr, "Notifications manager shutdown took longer than 5s, "+
1134+
"this may result in duplicate notifications being sent: %s\n", err)
1135+
} else {
1136+
cliui.Info(inv.Stdout, "Gracefully shut down notifications manager\n")
1137+
}
1138+
}
1139+
10911140
// Shut down provisioners before waiting for WebSockets
10921141
// connections to close.
10931142
var wg sync.WaitGroup
@@ -1227,6 +1276,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
12271276
return serverCmd
12281277
}
12291278

1279+
// templateHelpers builds a set of functions which can be called in templates.
1280+
// We build them here to avoid an import cycle by using coderd.Options in notifications.Manager.
1281+
// We can later use this to inject whitelabel fields when app name / logo URL are overridden.
1282+
func templateHelpers(options *coderd.Options) map[string]any {
1283+
return map[string]any{
1284+
"base_url": func() string { return options.AccessURL.String() },
1285+
}
1286+
}
1287+
12301288
// printDeprecatedOptions loops through all command options, and prints
12311289
// a warning for usage of deprecated options.
12321290
func PrintDeprecatedOptions() serpent.MiddlewareFunc {

0 commit comments

Comments
 (0)