From bbd2dd80e22ec236e68417809d21b67168958926 Mon Sep 17 00:00:00 2001 From: Kayla Washburn-Love Date: Tue, 29 Oct 2024 12:30:13 -0600 Subject: [PATCH 001/223] fix: show template name on workspace page when template display name is unset (#15262) --- .../WorkspacePage/WorkspaceTopbar.stories.tsx | 36 +++++++++++++++++++ .../pages/WorkspacePage/WorkspaceTopbar.tsx | 12 ++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index ef7c72895552b..d95cfc3d60daf 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -320,3 +320,39 @@ export const TemplateDoesNotAllowAutostop: Story = { }, }, }; + +export const TemplateInfoPopover: Story = { + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step("activate hover trigger", async () => { + await userEvent.hover(canvas.getByText(baseWorkspace.name)); + await waitFor(() => + expect( + canvas.getByRole("presentation", { hidden: true }), + ).toHaveTextContent(MockTemplate.display_name), + ); + }); + }, +}; + +export const TemplateInfoPopoverWithoutDisplayName: Story = { + args: { + workspace: { + ...baseWorkspace, + template_display_name: "", + }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step("activate hover trigger", async () => { + await userEvent.hover(canvas.getByText(baseWorkspace.name)); + await waitFor(() => + expect( + canvas.getByRole("presentation", { hidden: true }), + ).toHaveTextContent(MockTemplate.name), + ); + }); + }, +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index e3be26462cc5b..7ca112befb4e5 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -160,7 +160,9 @@ export const WorkspaceTopbar: FC = ({ templateIconUrl={workspace.template_icon} rootTemplateUrl={templateLink} templateVersionName={workspace.latest_build.template_version_name} - templateVersionDisplayName={workspace.template_display_name} + templateDisplayName={ + workspace.template_display_name || workspace.template_name + } latestBuildVersionName={ workspace.latest_build.template_version_name } @@ -366,7 +368,7 @@ type WorkspaceBreadcrumbProps = Readonly<{ rootTemplateUrl: string; templateVersionName: string; latestBuildVersionName: string; - templateVersionDisplayName?: string; + templateDisplayName: string; }>; const WorkspaceBreadcrumb: FC = ({ @@ -375,7 +377,7 @@ const WorkspaceBreadcrumb: FC = ({ rootTemplateUrl, templateVersionName, latestBuildVersionName, - templateVersionDisplayName = templateVersionName, + templateDisplayName, }) => { return ( @@ -399,7 +401,7 @@ const WorkspaceBreadcrumb: FC = ({ to={rootTemplateUrl} css={{ color: "inherit" }} > - {templateVersionDisplayName} + {templateDisplayName} } subtitle={ @@ -419,7 +421,7 @@ const WorkspaceBreadcrumb: FC = ({ fitImage /> } - imgFallbackText={templateVersionDisplayName} + imgFallbackText={templateDisplayName} /> From 25738388d55ed3bfc38495752a4ea044a0e3a75c Mon Sep 17 00:00:00 2001 From: Stephen Kirby <58410745+stirby@users.noreply.github.com> Date: Tue, 29 Oct 2024 20:58:26 -0500 Subject: [PATCH 002/223] chore(docs): add documentation on custom roles (#15280) These docs were overwritten in the restructure merge. --- docs/admin/users/groups-roles.md | 24 ++++++++++++++++++ .../users/roles/assigning-custom-role.PNG | Bin 0 -> 64037 bytes .../users/roles/creating-custom-role.PNG | Bin 0 -> 84273 bytes .../images/admin/users/roles/custom-roles.PNG | Bin 0 -> 107693 bytes 4 files changed, 24 insertions(+) create mode 100644 docs/images/admin/users/roles/assigning-custom-role.PNG create mode 100644 docs/images/admin/users/roles/creating-custom-role.PNG create mode 100644 docs/images/admin/users/roles/custom-roles.PNG diff --git a/docs/admin/users/groups-roles.md b/docs/admin/users/groups-roles.md index 77dd35bf9dd89..17c0fc8b5b8b9 100644 --- a/docs/admin/users/groups-roles.md +++ b/docs/admin/users/groups-roles.md @@ -31,6 +31,30 @@ Roles determine which actions users can take within the platform. A user may have one or more roles. All users have an implicit Member role that may use personal workspaces. +## Custom Roles (Premium) (Beta) + +Starting in v2.16.0, Premium Coder deployments can configure custom roles on the +[Organization](./organizations.md) level. You can create and assign custom roles +in the dashboard under **Organizations** -> **My Organization** -> **Roles**. + +> Note: This requires a Premium license. +> [Contact your account team](https://coder.com/contact) for more details. + +![Custom roles](../../images/admin/users/roles/custom-roles.PNG) + +Clicking "Create custom role" opens a UI to select the desired permissions for a +given persona. + +![Creating a custom role](../../images/admin/users/roles/creating-custom-role.PNG) + +From there, you can assign the custom role to any user in the organization under +the **Users** settings in the dashboard. + +![Assigning a custom role](../../images/admin/users/roles/assigning-custom-role.PNG) + +Note that these permissions only apply to the scope of an +[organization](./organizations.md), not across the deployment. + ### Security notes A malicious Template Admin could write a template that executes commands on the diff --git a/docs/images/admin/users/roles/assigning-custom-role.PNG b/docs/images/admin/users/roles/assigning-custom-role.PNG new file mode 100644 index 0000000000000000000000000000000000000000..271f1bcae7781f353c1ab2db101cd508655787c0 GIT binary patch literal 64037 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 b>}wxC%TF+27Z?Pe4!|M|NU$1$ZWPS2cnV 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 literal 0 HcmV?d00001 From 823a2ea22ef3e6c65acd8328df9d501d9f8af8e4 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 30 Oct 2024 10:06:10 +0000 Subject: [PATCH 003/223] chore(cli): drop 'notification' prefix for configuring email auth (#15270) Closes https://github.com/coder/coder/issues/14644 --- cli/testdata/coder_server_--help.golden | 72 ++++++- cli/testdata/server-config.yaml.golden | 51 ++++- coderd/notifications/dispatch/smtp.go | 4 +- codersdk/deployment.go | 178 +++++++++++++++++- codersdk/deployment_test.go | 3 + docs/admin/monitoring/notifications/index.md | 56 +++--- docs/reference/cli/server.md | 145 +++++++++++++- .../cli/testdata/coder_server_--help.golden | 72 ++++++- 8 files changed, 536 insertions(+), 45 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index d5c26d98115cb..cd647d0537a93 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -106,6 +106,58 @@ Use a YAML configuration file when your server launch become unwieldy. Write out the current server config as YAML to stdout. +EMAIL OPTIONS: +Configure how emails are sent. + + --email-force-tls bool, $CODER_EMAIL_FORCE_TLS (default: false) + Force a TLS connection to the configured SMTP smarthost. + + --email-from string, $CODER_EMAIL_FROM + The sender's address to use. + + --email-hello string, $CODER_EMAIL_HELLO (default: localhost) + The hostname identifying the SMTP server. + + --email-smarthost host:port, $CODER_EMAIL_SMARTHOST (default: localhost:587) + The intermediary SMTP host through which emails are sent. + +EMAIL / EMAIL AUTHENTICATION OPTIONS: +Configure SMTP authentication options. + + --email-auth-identity string, $CODER_EMAIL_AUTH_IDENTITY + Identity to use with PLAIN authentication. + + --email-auth-password string, $CODER_EMAIL_AUTH_PASSWORD + Password to use with PLAIN/LOGIN authentication. + + --email-auth-password-file string, $CODER_EMAIL_AUTH_PASSWORD_FILE + File from which to load password for use with PLAIN/LOGIN + authentication. + + --email-auth-username string, $CODER_EMAIL_AUTH_USERNAME + Username to use with PLAIN/LOGIN authentication. + +EMAIL / EMAIL TLS OPTIONS: +Configure TLS for your SMTP server target. + + --email-tls-ca-cert-file string, $CODER_EMAIL_TLS_CACERTFILE + CA certificate file to use. + + --email-tls-cert-file string, $CODER_EMAIL_TLS_CERTFILE + Certificate file to use. + + --email-tls-cert-key-file string, $CODER_EMAIL_TLS_CERTKEYFILE + Certificate key file to use. + + --email-tls-server-name string, $CODER_EMAIL_TLS_SERVERNAME + Server name to verify against the target certificate. + + --email-tls-skip-verify bool, $CODER_EMAIL_TLS_SKIPVERIFY + Skip verification of the target server's certificate (insecure). + + --email-tls-starttls bool, $CODER_EMAIL_TLS_STARTTLS + Enable STARTTLS to upgrade insecure SMTP connections using TLS. + INTROSPECTION / HEALTH CHECK OPTIONS: --health-check-refresh duration, $CODER_HEALTH_CHECK_REFRESH (default: 10m0s) Refresh interval for healthchecks. @@ -349,54 +401,68 @@ Configure how notifications are processed and delivered. NOTIFICATIONS / EMAIL OPTIONS: Configure how email notifications are sent. - --notifications-email-force-tls bool, $CODER_NOTIFICATIONS_EMAIL_FORCE_TLS (default: false) + --notifications-email-force-tls bool, $CODER_NOTIFICATIONS_EMAIL_FORCE_TLS Force a TLS connection to the configured SMTP smarthost. + DEPRECATED: Use --email-force-tls instead. --notifications-email-from string, $CODER_NOTIFICATIONS_EMAIL_FROM The sender's address to use. + DEPRECATED: Use --email-from instead. - --notifications-email-hello string, $CODER_NOTIFICATIONS_EMAIL_HELLO (default: localhost) + --notifications-email-hello string, $CODER_NOTIFICATIONS_EMAIL_HELLO The hostname identifying the SMTP server. + DEPRECATED: Use --email-hello instead. - --notifications-email-smarthost host:port, $CODER_NOTIFICATIONS_EMAIL_SMARTHOST (default: localhost:587) + --notifications-email-smarthost host:port, $CODER_NOTIFICATIONS_EMAIL_SMARTHOST The intermediary SMTP host through which emails are sent. + DEPRECATED: Use --email-smarthost instead. NOTIFICATIONS / EMAIL / EMAIL AUTHENTICATION OPTIONS: Configure SMTP authentication options. --notifications-email-auth-identity string, $CODER_NOTIFICATIONS_EMAIL_AUTH_IDENTITY Identity to use with PLAIN authentication. + DEPRECATED: Use --email-auth-identity instead. --notifications-email-auth-password string, $CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD Password to use with PLAIN/LOGIN authentication. + DEPRECATED: Use --email-auth-password instead. --notifications-email-auth-password-file string, $CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD_FILE File from which to load password for use with PLAIN/LOGIN authentication. + DEPRECATED: Use --email-auth-password-file instead. --notifications-email-auth-username string, $CODER_NOTIFICATIONS_EMAIL_AUTH_USERNAME Username to use with PLAIN/LOGIN authentication. + DEPRECATED: Use --email-auth-username instead. NOTIFICATIONS / EMAIL / EMAIL TLS OPTIONS: Configure TLS for your SMTP server target. --notifications-email-tls-ca-cert-file string, $CODER_NOTIFICATIONS_EMAIL_TLS_CACERTFILE CA certificate file to use. + DEPRECATED: Use --email-tls-ca-cert-file instead. --notifications-email-tls-cert-file string, $CODER_NOTIFICATIONS_EMAIL_TLS_CERTFILE Certificate file to use. + DEPRECATED: Use --email-tls-cert-file instead. --notifications-email-tls-cert-key-file string, $CODER_NOTIFICATIONS_EMAIL_TLS_CERTKEYFILE Certificate key file to use. + DEPRECATED: Use --email-tls-cert-key-file instead. --notifications-email-tls-server-name string, $CODER_NOTIFICATIONS_EMAIL_TLS_SERVERNAME Server name to verify against the target certificate. + DEPRECATED: Use --email-tls-server-name instead. --notifications-email-tls-skip-verify bool, $CODER_NOTIFICATIONS_EMAIL_TLS_SKIPVERIFY Skip verification of the target server's certificate (insecure). + DEPRECATED: Use --email-tls-skip-verify instead. --notifications-email-tls-starttls bool, $CODER_NOTIFICATIONS_EMAIL_TLS_STARTTLS Enable STARTTLS to upgrade insecure SMTP connections using TLS. + DEPRECATED: Use --email-tls-starttls instead. NOTIFICATIONS / WEBHOOK OPTIONS: --notifications-webhook-endpoint url, $CODER_NOTIFICATIONS_WEBHOOK_ENDPOINT diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 95486a26344b8..38b2b68c24de1 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -518,6 +518,51 @@ userQuietHoursSchedule: # compatibility reasons, this will be removed in a future release. # (default: false, type: bool) allowWorkspaceRenames: false +# Configure how emails are sent. +email: + # The sender's address to use. + # (default: , type: string) + from: "" + # The intermediary SMTP host through which emails are sent. + # (default: localhost:587, type: host:port) + smarthost: localhost:587 + # The hostname identifying the SMTP server. + # (default: localhost, type: string) + hello: localhost + # Force a TLS connection to the configured SMTP smarthost. + # (default: false, type: bool) + forceTLS: false + # Configure SMTP authentication options. + emailAuth: + # Identity to use with PLAIN authentication. + # (default: , type: string) + identity: "" + # Username to use with PLAIN/LOGIN authentication. + # (default: , type: string) + username: "" + # File from which to load password for use with PLAIN/LOGIN authentication. + # (default: , type: string) + passwordFile: "" + # Configure TLS for your SMTP server target. + emailTLS: + # Enable STARTTLS to upgrade insecure SMTP connections using TLS. + # (default: , type: bool) + startTLS: false + # Server name to verify against the target certificate. + # (default: , type: string) + serverName: "" + # Skip verification of the target server's certificate (insecure). + # (default: , type: bool) + insecureSkipVerify: false + # CA certificate file to use. + # (default: , type: string) + caCertFile: "" + # Certificate file to use. + # (default: , type: string) + certFile: "" + # Certificate key file to use. + # (default: , type: string) + certKeyFile: "" # Configure how notifications are processed and delivered. notifications: # Which delivery method to use (available options: 'smtp', 'webhook'). @@ -532,13 +577,13 @@ notifications: # (default: , type: string) from: "" # The intermediary SMTP host through which emails are sent. - # (default: localhost:587, type: host:port) + # (default: , type: host:port) smarthost: localhost:587 # The hostname identifying the SMTP server. - # (default: localhost, type: string) + # (default: , type: string) hello: localhost # Force a TLS connection to the configured SMTP smarthost. - # (default: false, type: bool) + # (default: , type: bool) forceTLS: false # Configure SMTP authentication options. emailAuth: diff --git a/coderd/notifications/dispatch/smtp.go b/coderd/notifications/dispatch/smtp.go index e18aeaef88b81..dfb628b62eb86 100644 --- a/coderd/notifications/dispatch/smtp.go +++ b/coderd/notifications/dispatch/smtp.go @@ -453,7 +453,7 @@ func (s *SMTPHandler) auth(ctx context.Context, mechs string) (sasl.Client, erro continue } if password == "" { - errs = multierror.Append(errs, xerrors.New("cannot use PLAIN auth, password not defined (see CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD)")) + errs = multierror.Append(errs, xerrors.New("cannot use PLAIN auth, password not defined (see CODER_EMAIL_AUTH_PASSWORD)")) continue } @@ -475,7 +475,7 @@ func (s *SMTPHandler) auth(ctx context.Context, mechs string) (sasl.Client, erro continue } if password == "" { - errs = multierror.Append(errs, xerrors.New("cannot use LOGIN auth, password not defined (see CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD)")) + errs = multierror.Append(errs, xerrors.New("cannot use LOGIN auth, password not defined (see CODER_EMAIL_AUTH_PASSWORD)")) continue } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 6a5f7c52ac8f5..3ba09bd38d1a4 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -926,6 +926,23 @@ when required by your organization's security policy.`, Name: "Config", Description: `Use a YAML configuration file when your server launch become unwieldy.`, } + deploymentGroupEmail = serpent.Group{ + Name: "Email", + Description: "Configure how emails are sent.", + YAML: "email", + } + deploymentGroupEmailAuth = serpent.Group{ + Name: "Email Authentication", + Parent: &deploymentGroupEmail, + Description: "Configure SMTP authentication options.", + YAML: "emailAuth", + } + deploymentGroupEmailTLS = serpent.Group{ + Name: "Email TLS", + Parent: &deploymentGroupEmail, + Description: "Configure TLS for your SMTP server target.", + YAML: "emailTLS", + } deploymentGroupNotifications = serpent.Group{ Name: "Notifications", YAML: "notifications", @@ -997,6 +1014,135 @@ when required by your organization's security policy.`, Group: &deploymentGroupIntrospectionLogging, YAML: "filter", } + emailFrom := serpent.Option{ + Name: "Email: From Address", + Description: "The sender's address to use.", + Flag: "email-from", + Env: "CODER_EMAIL_FROM", + Value: &c.Notifications.SMTP.From, + Group: &deploymentGroupEmail, + YAML: "from", + } + emailSmarthost := serpent.Option{ + Name: "Email: Smarthost", + Description: "The intermediary SMTP host through which emails are sent.", + Flag: "email-smarthost", + Env: "CODER_EMAIL_SMARTHOST", + Default: "localhost:587", // To pass validation. + Value: &c.Notifications.SMTP.Smarthost, + Group: &deploymentGroupEmail, + YAML: "smarthost", + } + emailHello := serpent.Option{ + Name: "Email: Hello", + Description: "The hostname identifying the SMTP server.", + Flag: "email-hello", + Env: "CODER_EMAIL_HELLO", + Default: "localhost", + Value: &c.Notifications.SMTP.Hello, + Group: &deploymentGroupEmail, + YAML: "hello", + } + emailForceTLS := serpent.Option{ + Name: "Email: Force TLS", + Description: "Force a TLS connection to the configured SMTP smarthost.", + Flag: "email-force-tls", + Env: "CODER_EMAIL_FORCE_TLS", + Default: "false", + Value: &c.Notifications.SMTP.ForceTLS, + Group: &deploymentGroupEmail, + YAML: "forceTLS", + } + emailAuthIdentity := serpent.Option{ + Name: "Email Auth: Identity", + Description: "Identity to use with PLAIN authentication.", + Flag: "email-auth-identity", + Env: "CODER_EMAIL_AUTH_IDENTITY", + Value: &c.Notifications.SMTP.Auth.Identity, + Group: &deploymentGroupEmailAuth, + YAML: "identity", + } + emailAuthUsername := serpent.Option{ + Name: "Email Auth: Username", + Description: "Username to use with PLAIN/LOGIN authentication.", + Flag: "email-auth-username", + Env: "CODER_EMAIL_AUTH_USERNAME", + Value: &c.Notifications.SMTP.Auth.Username, + Group: &deploymentGroupEmailAuth, + YAML: "username", + } + emailAuthPassword := serpent.Option{ + Name: "Email Auth: Password", + Description: "Password to use with PLAIN/LOGIN authentication.", + Flag: "email-auth-password", + Env: "CODER_EMAIL_AUTH_PASSWORD", + Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"), + Value: &c.Notifications.SMTP.Auth.Password, + Group: &deploymentGroupEmailAuth, + } + emailAuthPasswordFile := serpent.Option{ + Name: "Email Auth: Password File", + Description: "File from which to load password for use with PLAIN/LOGIN authentication.", + Flag: "email-auth-password-file", + Env: "CODER_EMAIL_AUTH_PASSWORD_FILE", + Value: &c.Notifications.SMTP.Auth.PasswordFile, + Group: &deploymentGroupEmailAuth, + YAML: "passwordFile", + } + emailTLSStartTLS := serpent.Option{ + Name: "Email TLS: StartTLS", + Description: "Enable STARTTLS to upgrade insecure SMTP connections using TLS.", + Flag: "email-tls-starttls", + Env: "CODER_EMAIL_TLS_STARTTLS", + Value: &c.Notifications.SMTP.TLS.StartTLS, + Group: &deploymentGroupEmailTLS, + YAML: "startTLS", + } + emailTLSServerName := serpent.Option{ + Name: "Email TLS: Server Name", + Description: "Server name to verify against the target certificate.", + Flag: "email-tls-server-name", + Env: "CODER_EMAIL_TLS_SERVERNAME", + Value: &c.Notifications.SMTP.TLS.ServerName, + Group: &deploymentGroupEmailTLS, + YAML: "serverName", + } + emailTLSSkipCertVerify := serpent.Option{ + Name: "Email TLS: Skip Certificate Verification (Insecure)", + Description: "Skip verification of the target server's certificate (insecure).", + Flag: "email-tls-skip-verify", + Env: "CODER_EMAIL_TLS_SKIPVERIFY", + Value: &c.Notifications.SMTP.TLS.InsecureSkipVerify, + Group: &deploymentGroupEmailTLS, + YAML: "insecureSkipVerify", + } + emailTLSCertAuthorityFile := serpent.Option{ + Name: "Email TLS: Certificate Authority File", + Description: "CA certificate file to use.", + Flag: "email-tls-ca-cert-file", + Env: "CODER_EMAIL_TLS_CACERTFILE", + Value: &c.Notifications.SMTP.TLS.CAFile, + Group: &deploymentGroupEmailTLS, + YAML: "caCertFile", + } + emailTLSCertFile := serpent.Option{ + Name: "Email TLS: Certificate File", + Description: "Certificate file to use.", + Flag: "email-tls-cert-file", + Env: "CODER_EMAIL_TLS_CERTFILE", + Value: &c.Notifications.SMTP.TLS.CertFile, + Group: &deploymentGroupEmailTLS, + YAML: "certFile", + } + emailTLSCertKeyFile := serpent.Option{ + Name: "Email TLS: Certificate Key File", + Description: "Certificate key file to use.", + Flag: "email-tls-cert-key-file", + Env: "CODER_EMAIL_TLS_CERTKEYFILE", + Value: &c.Notifications.SMTP.TLS.KeyFile, + Group: &deploymentGroupEmailTLS, + YAML: "certKeyFile", + } opts := serpent.OptionSet{ { Name: "Access URL", @@ -2432,6 +2578,21 @@ Write out the current server config as YAML to stdout.`, YAML: "thresholdDatabase", Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"), }, + // Email options + emailFrom, + emailSmarthost, + emailHello, + emailForceTLS, + emailAuthIdentity, + emailAuthUsername, + emailAuthPassword, + emailAuthPasswordFile, + emailTLSStartTLS, + emailTLSServerName, + emailTLSSkipCertVerify, + emailTLSCertAuthorityFile, + emailTLSCertFile, + emailTLSCertKeyFile, // Notifications Options { Name: "Notifications: Method", @@ -2462,36 +2623,37 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.From, Group: &deploymentGroupNotificationsEmail, YAML: "from", + UseInstead: serpent.OptionSet{emailFrom}, }, { Name: "Notifications: Email: Smarthost", Description: "The intermediary SMTP host through which emails are sent.", Flag: "notifications-email-smarthost", Env: "CODER_NOTIFICATIONS_EMAIL_SMARTHOST", - Default: "localhost:587", // To pass validation. Value: &c.Notifications.SMTP.Smarthost, Group: &deploymentGroupNotificationsEmail, YAML: "smarthost", + UseInstead: serpent.OptionSet{emailSmarthost}, }, { Name: "Notifications: Email: Hello", Description: "The hostname identifying the SMTP server.", Flag: "notifications-email-hello", Env: "CODER_NOTIFICATIONS_EMAIL_HELLO", - Default: "localhost", Value: &c.Notifications.SMTP.Hello, Group: &deploymentGroupNotificationsEmail, YAML: "hello", + UseInstead: serpent.OptionSet{emailHello}, }, { Name: "Notifications: Email: Force TLS", Description: "Force a TLS connection to the configured SMTP smarthost.", Flag: "notifications-email-force-tls", Env: "CODER_NOTIFICATIONS_EMAIL_FORCE_TLS", - Default: "false", Value: &c.Notifications.SMTP.ForceTLS, Group: &deploymentGroupNotificationsEmail, YAML: "forceTLS", + UseInstead: serpent.OptionSet{emailForceTLS}, }, { Name: "Notifications: Email Auth: Identity", @@ -2501,6 +2663,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.Auth.Identity, Group: &deploymentGroupNotificationsEmailAuth, YAML: "identity", + UseInstead: serpent.OptionSet{emailAuthIdentity}, }, { Name: "Notifications: Email Auth: Username", @@ -2510,6 +2673,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.Auth.Username, Group: &deploymentGroupNotificationsEmailAuth, YAML: "username", + UseInstead: serpent.OptionSet{emailAuthUsername}, }, { Name: "Notifications: Email Auth: Password", @@ -2519,6 +2683,7 @@ Write out the current server config as YAML to stdout.`, Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"), Value: &c.Notifications.SMTP.Auth.Password, Group: &deploymentGroupNotificationsEmailAuth, + UseInstead: serpent.OptionSet{emailAuthPassword}, }, { Name: "Notifications: Email Auth: Password File", @@ -2528,6 +2693,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.Auth.PasswordFile, Group: &deploymentGroupNotificationsEmailAuth, YAML: "passwordFile", + UseInstead: serpent.OptionSet{emailAuthPasswordFile}, }, { Name: "Notifications: Email TLS: StartTLS", @@ -2537,6 +2703,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.TLS.StartTLS, Group: &deploymentGroupNotificationsEmailTLS, YAML: "startTLS", + UseInstead: serpent.OptionSet{emailTLSStartTLS}, }, { Name: "Notifications: Email TLS: Server Name", @@ -2546,6 +2713,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.TLS.ServerName, Group: &deploymentGroupNotificationsEmailTLS, YAML: "serverName", + UseInstead: serpent.OptionSet{emailTLSServerName}, }, { Name: "Notifications: Email TLS: Skip Certificate Verification (Insecure)", @@ -2555,6 +2723,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.TLS.InsecureSkipVerify, Group: &deploymentGroupNotificationsEmailTLS, YAML: "insecureSkipVerify", + UseInstead: serpent.OptionSet{emailTLSSkipCertVerify}, }, { Name: "Notifications: Email TLS: Certificate Authority File", @@ -2564,6 +2733,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.TLS.CAFile, Group: &deploymentGroupNotificationsEmailTLS, YAML: "caCertFile", + UseInstead: serpent.OptionSet{emailTLSCertAuthorityFile}, }, { Name: "Notifications: Email TLS: Certificate File", @@ -2573,6 +2743,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.TLS.CertFile, Group: &deploymentGroupNotificationsEmailTLS, YAML: "certFile", + UseInstead: serpent.OptionSet{emailTLSCertFile}, }, { Name: "Notifications: Email TLS: Certificate Key File", @@ -2582,6 +2753,7 @@ Write out the current server config as YAML to stdout.`, Value: &c.Notifications.SMTP.TLS.KeyFile, Group: &deploymentGroupNotificationsEmailTLS, YAML: "certKeyFile", + UseInstead: serpent.OptionSet{emailTLSCertKeyFile}, }, { Name: "Notifications: Webhook: Endpoint", diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go index d7eca6323000c..61474a3b77ea1 100644 --- a/codersdk/deployment_test.go +++ b/codersdk/deployment_test.go @@ -78,6 +78,9 @@ func TestDeploymentValues_HighlyConfigurable(t *testing.T) { "Provisioner Daemon Pre-shared Key (PSK)": { yaml: true, }, + "Email Auth: Password": { + yaml: true, + }, "Notifications: Email Auth: Password": { yaml: true, }, diff --git a/docs/admin/monitoring/notifications/index.md b/docs/admin/monitoring/notifications/index.md index a98fa0b3e8b48..eabc09438d7b9 100644 --- a/docs/admin/monitoring/notifications/index.md +++ b/docs/admin/monitoring/notifications/index.md @@ -89,34 +89,34 @@ existing one. **Server Settings:** -| Required | CLI | Env | Type | Description | Default | -| :------: | --------------------------------- | ------------------------------------- | ----------- | ----------------------------------------- | ------------- | -| ✔️ | `--notifications-email-from` | `CODER_NOTIFICATIONS_EMAIL_FROM` | `string` | The sender's address to use. | | -| ✔️ | `--notifications-email-smarthost` | `CODER_NOTIFICATIONS_EMAIL_SMARTHOST` | `host:port` | The SMTP relay to send messages through. | localhost:587 | -| ✔️ | `--notifications-email-hello` | `CODER_NOTIFICATIONS_EMAIL_HELLO` | `string` | The hostname identifying the SMTP server. | localhost | +| Required | CLI | Env | Type | Description | Default | +| :------: | ------------------- | ----------------------- | ----------- | ----------------------------------------- | ------------- | +| ✔️ | `--email-from` | `CODER_EMAIL_FROM` | `string` | The sender's address to use. | | +| ✔️ | `--email-smarthost` | `CODER_EMAIL_SMARTHOST` | `host:port` | The SMTP relay to send messages through. | localhost:587 | +| ✔️ | `--email-hello` | `CODER_EMAIL_HELLO` | `string` | The hostname identifying the SMTP server. | localhost | **Authentication Settings:** -| Required | CLI | Env | Type | Description | -| :------: | ------------------------------------------ | ---------------------------------------------- | -------- | ------------------------------------------------------------------------- | -| - | `--notifications-email-auth-username` | `CODER_NOTIFICATIONS_EMAIL_AUTH_USERNAME` | `string` | Username to use with PLAIN/LOGIN authentication. | -| - | `--notifications-email-auth-password` | `CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD` | `string` | Password to use with PLAIN/LOGIN authentication. | -| - | `--notifications-email-auth-password-file` | `CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD_FILE` | `string` | File from which to load password for use with PLAIN/LOGIN authentication. | -| - | `--notifications-email-auth-identity` | `CODER_NOTIFICATIONS_EMAIL_AUTH_IDENTITY` | `string` | Identity to use with PLAIN authentication. | +| Required | CLI | Env | Type | Description | +| :------: | ---------------------------- | -------------------------------- | -------- | ------------------------------------------------------------------------- | +| - | `--email-auth-username` | `CODER_EMAIL_AUTH_USERNAME` | `string` | Username to use with PLAIN/LOGIN authentication. | +| - | `--email-auth-password` | `CODER_EMAIL_AUTH_PASSWORD` | `string` | Password to use with PLAIN/LOGIN authentication. | +| - | `--email-auth-password-file` | `CODER_EMAIL_AUTH_PASSWORD_FILE` | `string` | File from which to load password for use with PLAIN/LOGIN authentication. | +| - | `--email-auth-identity` | `CODER_EMAIL_AUTH_IDENTITY` | `string` | Identity to use with PLAIN authentication. | **TLS Settings:** -| Required | CLI | Env | Type | Description | Default | -| :------: | ----------------------------------------- | ------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| - | `--notifications-email-force-tls` | `CODER_NOTIFICATIONS_EMAIL_FORCE_TLS` | `bool` | Force a TLS connection to the configured SMTP smarthost. If port 465 is used, TLS will be forced. See https://datatracker.ietf.org/doc/html/rfc8314#section-3.3. | false | -| - | `--notifications-email-tls-starttls` | `CODER_NOTIFICATIONS_EMAIL_TLS_STARTTLS` | `bool` | Enable STARTTLS to upgrade insecure SMTP connections using TLS. Ignored if `CODER_NOTIFICATIONS_EMAIL_FORCE_TLS` is set. | false | -| - | `--notifications-email-tls-skip-verify` | `CODER_NOTIFICATIONS_EMAIL_TLS_SKIPVERIFY` | `bool` | Skip verification of the target server's certificate (**insecure**). | false | -| - | `--notifications-email-tls-server-name` | `CODER_NOTIFICATIONS_EMAIL_TLS_SERVERNAME` | `string` | Server name to verify against the target certificate. | | -| - | `--notifications-email-tls-cert-file` | `CODER_NOTIFICATIONS_EMAIL_TLS_CERTFILE` | `string` | Certificate file to use. | | -| - | `--notifications-email-tls-cert-key-file` | `CODER_NOTIFICATIONS_EMAIL_TLS_CERTKEYFILE` | `string` | Certificate key file to use. | | +| Required | CLI | Env | Type | Description | Default | +| :------: | --------------------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| - | `--email-force-tls` | `CODER_EMAIL_FORCE_TLS` | `bool` | Force a TLS connection to the configured SMTP smarthost. If port 465 is used, TLS will be forced. See https://datatracker.ietf.org/doc/html/rfc8314#section-3.3. | false | +| - | `--email-tls-starttls` | `CODER_EMAIL_TLS_STARTTLS` | `bool` | Enable STARTTLS to upgrade insecure SMTP connections using TLS. Ignored if `CODER_NOTIFICATIONS_EMAIL_FORCE_TLS` is set. | false | +| - | `--email-tls-skip-verify` | `CODER_EMAIL_TLS_SKIPVERIFY` | `bool` | Skip verification of the target server's certificate (**insecure**). | false | +| - | `--email-tls-server-name` | `CODER_EMAIL_TLS_SERVERNAME` | `string` | Server name to verify against the target certificate. | | +| - | `--email-tls-cert-file` | `CODER_EMAIL_TLS_CERTFILE` | `string` | Certificate file to use. | | +| - | `--email-tls-cert-key-file` | `CODER_EMAIL_TLS_CERTKEYFILE` | `string` | Certificate key file to use. | | -**NOTE:** you _MUST_ use `CODER_NOTIFICATIONS_EMAIL_FORCE_TLS` if your smarthost -supports TLS on a port other than `465`. +**NOTE:** you _MUST_ use `CODER_EMAIL_FORCE_TLS` if your smarthost supports TLS +on a port other than `465`. ### Send emails using G-Suite @@ -126,9 +126,9 @@ After setting the required fields above: account you wish to send from 2. Set the following configuration options: ``` - CODER_NOTIFICATIONS_EMAIL_SMARTHOST=smtp.gmail.com:465 - CODER_NOTIFICATIONS_EMAIL_AUTH_USERNAME=@ - CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD="" + CODER_EMAIL_SMARTHOST=smtp.gmail.com:465 + CODER_EMAIL_AUTH_USERNAME=@ + CODER_EMAIL_AUTH_PASSWORD="" ``` See @@ -142,10 +142,10 @@ After setting the required fields above: 1. Setup an account on Microsoft 365 or outlook.com 2. Set the following configuration options: ``` - CODER_NOTIFICATIONS_EMAIL_SMARTHOST=smtp-mail.outlook.com:587 - CODER_NOTIFICATIONS_EMAIL_TLS_STARTTLS=true - CODER_NOTIFICATIONS_EMAIL_AUTH_USERNAME=@ - CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD="" + CODER_EMAIL_SMARTHOST=smtp-mail.outlook.com:587 + CODER_EMAIL_TLS_STARTTLS=true + CODER_EMAIL_AUTH_USERNAME=@ + CODER_EMAIL_AUTH_PASSWORD="" ``` See diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 981c2419cf903..42ef7f7418b45 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1249,6 +1249,148 @@ Refresh interval for healthchecks. The threshold for the database health check. If the median latency of the database exceeds this threshold over 5 attempts, the database is considered unhealthy. The default value is 15ms. +### --email-from + +| | | +| ----------- | ------------------------------ | +| Type | string | +| Environment | $CODER_EMAIL_FROM | +| YAML | email.from | + +The sender's address to use. + +### --email-smarthost + +| | | +| ----------- | ----------------------------------- | +| Type | host:port | +| Environment | $CODER_EMAIL_SMARTHOST | +| YAML | email.smarthost | +| Default | localhost:587 | + +The intermediary SMTP host through which emails are sent. + +### --email-hello + +| | | +| ----------- | ------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_HELLO | +| YAML | email.hello | +| Default | localhost | + +The hostname identifying the SMTP server. + +### --email-force-tls + +| | | +| ----------- | ----------------------------------- | +| Type | bool | +| Environment | $CODER_EMAIL_FORCE_TLS | +| YAML | email.forceTLS | +| Default | false | + +Force a TLS connection to the configured SMTP smarthost. + +### --email-auth-identity + +| | | +| ----------- | --------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_AUTH_IDENTITY | +| YAML | email.emailAuth.identity | + +Identity to use with PLAIN authentication. + +### --email-auth-username + +| | | +| ----------- | --------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_AUTH_USERNAME | +| YAML | email.emailAuth.username | + +Username to use with PLAIN/LOGIN authentication. + +### --email-auth-password + +| | | +| ----------- | --------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_AUTH_PASSWORD | + +Password to use with PLAIN/LOGIN authentication. + +### --email-auth-password-file + +| | | +| ----------- | -------------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_AUTH_PASSWORD_FILE | +| YAML | email.emailAuth.passwordFile | + +File from which to load password for use with PLAIN/LOGIN authentication. + +### --email-tls-starttls + +| | | +| ----------- | -------------------------------------- | +| Type | bool | +| Environment | $CODER_EMAIL_TLS_STARTTLS | +| YAML | email.emailTLS.startTLS | + +Enable STARTTLS to upgrade insecure SMTP connections using TLS. + +### --email-tls-server-name + +| | | +| ----------- | ---------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_TLS_SERVERNAME | +| YAML | email.emailTLS.serverName | + +Server name to verify against the target certificate. + +### --email-tls-skip-verify + +| | | +| ----------- | ---------------------------------------------- | +| Type | bool | +| Environment | $CODER_EMAIL_TLS_SKIPVERIFY | +| YAML | email.emailTLS.insecureSkipVerify | + +Skip verification of the target server's certificate (insecure). + +### --email-tls-ca-cert-file + +| | | +| ----------- | ---------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_TLS_CACERTFILE | +| YAML | email.emailTLS.caCertFile | + +CA certificate file to use. + +### --email-tls-cert-file + +| | | +| ----------- | -------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_TLS_CERTFILE | +| YAML | email.emailTLS.certFile | + +Certificate file to use. + +### --email-tls-cert-key-file + +| | | +| ----------- | ----------------------------------------- | +| Type | string | +| Environment | $CODER_EMAIL_TLS_CERTKEYFILE | +| YAML | email.emailTLS.certKeyFile | + +Certificate key file to use. + ### --notifications-method | | | @@ -1288,7 +1430,6 @@ The sender's address to use. | Type | host:port | | Environment | $CODER_NOTIFICATIONS_EMAIL_SMARTHOST | | YAML | notifications.email.smarthost | -| Default | localhost:587 | The intermediary SMTP host through which emails are sent. @@ -1299,7 +1440,6 @@ The intermediary SMTP host through which emails are sent. | Type | string | | Environment | $CODER_NOTIFICATIONS_EMAIL_HELLO | | YAML | notifications.email.hello | -| Default | localhost | The hostname identifying the SMTP server. @@ -1310,7 +1450,6 @@ The hostname identifying the SMTP server. | Type | bool | | Environment | $CODER_NOTIFICATIONS_EMAIL_FORCE_TLS | | YAML | notifications.email.forceTLS | -| Default | false | Force a TLS connection to the configured SMTP smarthost. diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index b637a0da3f74d..a6398586fa972 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -107,6 +107,58 @@ Use a YAML configuration file when your server launch become unwieldy. Write out the current server config as YAML to stdout. +EMAIL OPTIONS: +Configure how emails are sent. + + --email-force-tls bool, $CODER_EMAIL_FORCE_TLS (default: false) + Force a TLS connection to the configured SMTP smarthost. + + --email-from string, $CODER_EMAIL_FROM + The sender's address to use. + + --email-hello string, $CODER_EMAIL_HELLO (default: localhost) + The hostname identifying the SMTP server. + + --email-smarthost host:port, $CODER_EMAIL_SMARTHOST (default: localhost:587) + The intermediary SMTP host through which emails are sent. + +EMAIL / EMAIL AUTHENTICATION OPTIONS: +Configure SMTP authentication options. + + --email-auth-identity string, $CODER_EMAIL_AUTH_IDENTITY + Identity to use with PLAIN authentication. + + --email-auth-password string, $CODER_EMAIL_AUTH_PASSWORD + Password to use with PLAIN/LOGIN authentication. + + --email-auth-password-file string, $CODER_EMAIL_AUTH_PASSWORD_FILE + File from which to load password for use with PLAIN/LOGIN + authentication. + + --email-auth-username string, $CODER_EMAIL_AUTH_USERNAME + Username to use with PLAIN/LOGIN authentication. + +EMAIL / EMAIL TLS OPTIONS: +Configure TLS for your SMTP server target. + + --email-tls-ca-cert-file string, $CODER_EMAIL_TLS_CACERTFILE + CA certificate file to use. + + --email-tls-cert-file string, $CODER_EMAIL_TLS_CERTFILE + Certificate file to use. + + --email-tls-cert-key-file string, $CODER_EMAIL_TLS_CERTKEYFILE + Certificate key file to use. + + --email-tls-server-name string, $CODER_EMAIL_TLS_SERVERNAME + Server name to verify against the target certificate. + + --email-tls-skip-verify bool, $CODER_EMAIL_TLS_SKIPVERIFY + Skip verification of the target server's certificate (insecure). + + --email-tls-starttls bool, $CODER_EMAIL_TLS_STARTTLS + Enable STARTTLS to upgrade insecure SMTP connections using TLS. + INTROSPECTION / HEALTH CHECK OPTIONS: --health-check-refresh duration, $CODER_HEALTH_CHECK_REFRESH (default: 10m0s) Refresh interval for healthchecks. @@ -350,54 +402,68 @@ Configure how notifications are processed and delivered. NOTIFICATIONS / EMAIL OPTIONS: Configure how email notifications are sent. - --notifications-email-force-tls bool, $CODER_NOTIFICATIONS_EMAIL_FORCE_TLS (default: false) + --notifications-email-force-tls bool, $CODER_NOTIFICATIONS_EMAIL_FORCE_TLS Force a TLS connection to the configured SMTP smarthost. + DEPRECATED: Use --email-force-tls instead. --notifications-email-from string, $CODER_NOTIFICATIONS_EMAIL_FROM The sender's address to use. + DEPRECATED: Use --email-from instead. - --notifications-email-hello string, $CODER_NOTIFICATIONS_EMAIL_HELLO (default: localhost) + --notifications-email-hello string, $CODER_NOTIFICATIONS_EMAIL_HELLO The hostname identifying the SMTP server. + DEPRECATED: Use --email-hello instead. - --notifications-email-smarthost host:port, $CODER_NOTIFICATIONS_EMAIL_SMARTHOST (default: localhost:587) + --notifications-email-smarthost host:port, $CODER_NOTIFICATIONS_EMAIL_SMARTHOST The intermediary SMTP host through which emails are sent. + DEPRECATED: Use --email-smarthost instead. NOTIFICATIONS / EMAIL / EMAIL AUTHENTICATION OPTIONS: Configure SMTP authentication options. --notifications-email-auth-identity string, $CODER_NOTIFICATIONS_EMAIL_AUTH_IDENTITY Identity to use with PLAIN authentication. + DEPRECATED: Use --email-auth-identity instead. --notifications-email-auth-password string, $CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD Password to use with PLAIN/LOGIN authentication. + DEPRECATED: Use --email-auth-password instead. --notifications-email-auth-password-file string, $CODER_NOTIFICATIONS_EMAIL_AUTH_PASSWORD_FILE File from which to load password for use with PLAIN/LOGIN authentication. + DEPRECATED: Use --email-auth-password-file instead. --notifications-email-auth-username string, $CODER_NOTIFICATIONS_EMAIL_AUTH_USERNAME Username to use with PLAIN/LOGIN authentication. + DEPRECATED: Use --email-auth-username instead. NOTIFICATIONS / EMAIL / EMAIL TLS OPTIONS: Configure TLS for your SMTP server target. --notifications-email-tls-ca-cert-file string, $CODER_NOTIFICATIONS_EMAIL_TLS_CACERTFILE CA certificate file to use. + DEPRECATED: Use --email-tls-ca-cert-file instead. --notifications-email-tls-cert-file string, $CODER_NOTIFICATIONS_EMAIL_TLS_CERTFILE Certificate file to use. + DEPRECATED: Use --email-tls-cert-file instead. --notifications-email-tls-cert-key-file string, $CODER_NOTIFICATIONS_EMAIL_TLS_CERTKEYFILE Certificate key file to use. + DEPRECATED: Use --email-tls-cert-key-file instead. --notifications-email-tls-server-name string, $CODER_NOTIFICATIONS_EMAIL_TLS_SERVERNAME Server name to verify against the target certificate. + DEPRECATED: Use --email-tls-server-name instead. --notifications-email-tls-skip-verify bool, $CODER_NOTIFICATIONS_EMAIL_TLS_SKIPVERIFY Skip verification of the target server's certificate (insecure). + DEPRECATED: Use --email-tls-skip-verify instead. --notifications-email-tls-starttls bool, $CODER_NOTIFICATIONS_EMAIL_TLS_STARTTLS Enable STARTTLS to upgrade insecure SMTP connections using TLS. + DEPRECATED: Use --email-tls-starttls instead. NOTIFICATIONS / WEBHOOK OPTIONS: --notifications-webhook-endpoint url, $CODER_NOTIFICATIONS_WEBHOOK_ENDPOINT From df6afd3354082a74ae6d36b8498fe127c4e332f8 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 30 Oct 2024 03:49:28 -0700 Subject: [PATCH 004/223] fix(install.sh): fix installation script for remote hosts (#15288) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 257576ae4d57a..40753f2f9973c 100755 --- a/install.sh +++ b/install.sh @@ -363,7 +363,7 @@ main() { if [ "${RSH_ARGS-}" ]; then RSH="${RSH-ssh}" echoh "Installing remotely with $RSH $RSH_ARGS" - curl -fsSL https://coder.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS" + curl -fsSL https://coder.com/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS" return fi From afacb07140a68f10f9507591340484b80235a038 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 30 Oct 2024 04:17:42 -0700 Subject: [PATCH 005/223] chore: tighten GitHub workflow permissions (#15282) --- .github/workflows/docker-base.yaml | 9 +++++---- .github/workflows/nightly-gauntlet.yaml | 4 ++++ .github/workflows/pr-cleanup.yaml | 6 +++--- .github/workflows/pr-deploy.yaml | 7 +++++-- .github/workflows/release-validation.yaml | 3 +++ .github/workflows/release.yaml | 14 ++++++++------ .github/workflows/stale.yaml | 15 +++++++++++++-- 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docker-base.yaml b/.github/workflows/docker-base.yaml index 8053b12780855..c0a3e87c5fe98 100644 --- a/.github/workflows/docker-base.yaml +++ b/.github/workflows/docker-base.yaml @@ -22,10 +22,6 @@ on: permissions: contents: read - # Necessary to push docker images to ghcr.io. - packages: write - # Necessary for depot.dev authentication. - id-token: write # Avoid running multiple jobs for the same commit. concurrency: @@ -33,6 +29,11 @@ concurrency: jobs: build: + permissions: + # Necessary for depot.dev authentication. + id-token: write + # Necessary to push docker images to ghcr.io. + packages: write runs-on: ubuntu-latest if: github.repository_owner == 'coder' steps: diff --git a/.github/workflows/nightly-gauntlet.yaml b/.github/workflows/nightly-gauntlet.yaml index 99ce3f62618a7..2b2887a13934e 100644 --- a/.github/workflows/nightly-gauntlet.yaml +++ b/.github/workflows/nightly-gauntlet.yaml @@ -6,6 +6,10 @@ on: # Every day at midnight - cron: "0 0 * * *" workflow_dispatch: + +permissions: + contents: read + jobs: go-race: # While GitHub's toaster runners are likelier to flake, we want consistency diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index ebcf097c0ef6b..f5cee03a4c6c4 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -8,12 +8,12 @@ on: description: "PR number" required: true -permissions: - packages: write - jobs: cleanup: runs-on: "ubuntu-latest" + permissions: + # Necessary to delete docker images from ghcr.io. + packages: write steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 6ca35c82eebeb..49e73e9b0bf63 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -30,8 +30,6 @@ env: permissions: contents: read - packages: write - pull-requests: write # needed for commenting on PRs jobs: check_pr: @@ -171,6 +169,8 @@ jobs: needs: get_info if: needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true' runs-on: "ubuntu-latest" + permissions: + pull-requests: write # needed for commenting on PRs steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 @@ -205,6 +205,9 @@ jobs: # Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag if: needs.get_info.outputs.BUILD == 'true' runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} + permissions: + # Necessary to push docker images to ghcr.io. + packages: write # This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs chnages. concurrency: group: build-${{ github.workflow }}-${{ github.ref }}-${{ needs.get_info.outputs.BUILD }} diff --git a/.github/workflows/release-validation.yaml b/.github/workflows/release-validation.yaml index 405e051f78526..2f12ac2bb5e7b 100644 --- a/.github/workflows/release-validation.yaml +++ b/.github/workflows/release-validation.yaml @@ -5,6 +5,9 @@ on: tags: - "v*" +permissions: + contents: read + jobs: network-performance: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b2757b25181d5..74b5b7b35a1e7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,12 +18,7 @@ on: default: false permissions: - # Required to publish a release - contents: write - # Necessary to push docker images to ghcr.io. - packages: write - # Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage) - id-token: write + contents: read concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -40,6 +35,13 @@ jobs: release: name: Build and publish runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} + permissions: + # Required to publish a release + contents: write + # Necessary to push docker images to ghcr.io. + packages: write + # Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage) + id-token: write env: # Necessary for Docker manifest DOCKER_CLI_EXPERIMENTAL: "enabled" diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index a05632d181ed3..d055c4f451e4e 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,16 +1,21 @@ -name: Stale Issue, Banch and Old Workflows Cleanup +name: Stale Issue, Branch and Old Workflows Cleanup on: schedule: # Every day at midnight - cron: "0 0 * * *" workflow_dispatch: + +permissions: + contents: read + jobs: issues: runs-on: ubuntu-latest permissions: + # Needed to close issues. issues: write + # Needed to close PRs. pull-requests: write - actions: write steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 @@ -86,6 +91,9 @@ jobs: branches: runs-on: ubuntu-latest + permissions: + # Needed to delete branches. + contents: write steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 @@ -105,6 +113,9 @@ jobs: exclude_open_pr_branches: true del_runs: runs-on: ubuntu-latest + permissions: + # Needed to delete workflow runs. + actions: write steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 From 144d3f3e3d6d6c6ee6bb752e02f74ee49a4b9d55 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 30 Oct 2024 10:20:47 -0400 Subject: [PATCH 006/223] chore: record lifecycle duration metric to prometheus (#15279) `autobuild_execution_duration_seconds` keeps track of how long autobuild takes and exposes it via prometheus histogram --- cli/server.go | 2 +- coderd/autobuild/lifecycle_executor.go | 23 ++++++++++++++++++++++- coderd/coderdtest/coderdtest.go | 1 + 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cli/server.go b/cli/server.go index b29b39b05fb4a..d0282004a2aa1 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1035,7 +1035,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value()) defer autobuildTicker.Stop() autobuildExecutor := autobuild.NewExecutor( - ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C, options.NotificationsEnqueuer) + ctx, options.Database, options.Pubsub, options.PrometheusRegistry, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C, options.NotificationsEnqueuer) autobuildExecutor.Run() hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value()) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index db3c1cfd3dd31..ac2930c9e32c8 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -10,6 +10,8 @@ import ( "github.com/dustin/go-humanize" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -39,6 +41,13 @@ type Executor struct { statsCh chan<- Stats // NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc. notificationsEnqueuer notifications.Enqueuer + reg prometheus.Registerer + + metrics executorMetrics +} + +type executorMetrics struct { + autobuildExecutionDuration prometheus.Histogram } // Stats contains information about one run of Executor. @@ -49,7 +58,8 @@ type Stats struct { } // New returns a new wsactions executor. -func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer) *Executor { +func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer) *Executor { + factory := promauto.With(reg) le := &Executor{ //nolint:gocritic // Autostart has a limited set of permissions. ctx: dbauthz.AsAutostart(ctx), @@ -61,6 +71,16 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss * auditor: auditor, accessControlStore: acs, notificationsEnqueuer: enqueuer, + reg: reg, + metrics: executorMetrics{ + autobuildExecutionDuration: factory.NewHistogram(prometheus.HistogramOpts{ + Namespace: "coderd", + Subsystem: "lifecycle", + Name: "autobuild_execution_duration_seconds", + Help: "Duration of each autobuild execution.", + Buckets: prometheus.DefBuckets, + }), + }, } return le } @@ -86,6 +106,7 @@ func (e *Executor) Run() { return } stats := e.runOnce(t) + e.metrics.autobuildExecutionDuration.Observe(stats.Elapsed.Seconds()) if e.statsCh != nil { select { case <-e.ctx.Done(): diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 47d9a42319d20..e287e04b8d0cf 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -335,6 +335,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can ctx, options.Database, options.Pubsub, + prometheus.NewRegistry(), &templateScheduleStore, &auditor, accessControlStore, From 371a2e12abef4e27406481f4ef38e6eb2550da24 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Thu, 31 Oct 2024 02:05:10 +1100 Subject: [PATCH 007/223] fix: check correct default during template push from stdin (#15293) I used the wrong default in #14643 - not sure how or why I didn't catch that.. --- cli/templatepush.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/templatepush.go b/cli/templatepush.go index f5ff1dcb3cf85..22a77791c5f77 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -282,7 +282,7 @@ func (pf *templateUploadFlags) stdin(inv *serpent.Invocation) (out bool) { } }() // We let the directory override our isTTY check - return pf.directory == "-" || (!isTTYIn(inv) && pf.directory == "") + return pf.directory == "-" || (!isTTYIn(inv) && pf.directory == ".") } func (pf *templateUploadFlags) upload(inv *serpent.Invocation, client *codersdk.Client) (*codersdk.UploadResponse, error) { From 85ff8e026763c8a8184400701677cb1a86a20336 Mon Sep 17 00:00:00 2001 From: Kayla Washburn-Love Date: Wed, 30 Oct 2024 10:07:19 -0600 Subject: [PATCH 008/223] chore: tweak e2e test timeouts (#15275) --- site/e2e/helpers.ts | 6 ++++-- site/e2e/playwright.config.ts | 8 ++------ site/e2e/tests/app.spec.ts | 4 +++- site/e2e/tests/outdatedAgent.spec.ts | 2 +- site/e2e/tests/outdatedCLI.spec.ts | 2 ++ site/e2e/tests/webTerminal.spec.ts | 2 ++ 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index fd436fa5dad7f..c5ac7f1abde65 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -425,7 +425,9 @@ export const startAgentWithCommand = async ( ); }); - await page.getByTestId("agent-status-ready").waitFor({ state: "visible" }); + await page + .getByTestId("agent-status-ready") + .waitFor({ state: "visible", timeout: 45_000 }); return cp; }; @@ -928,7 +930,7 @@ export async function openTerminalWindow( ): Promise { // Wait for the web terminal to open in a new tab const pagePromise = context.waitForEvent("page"); - await page.getByTestId("terminal").click(); + await page.getByTestId("terminal").click({ timeout: 60_000 }); const terminal = await pagePromise; await terminal.waitForLoadState("domcontentloaded"); diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 7042ebfcf5bb6..ea55bf398e7df 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -65,16 +65,12 @@ export default defineConfig({ testMatch: /.*\.spec\.ts/, dependencies: ["testsSetup"], use: { storageState }, - timeout: 50_000, + timeout: 30_000, }, ], reporter: [["./reporter.ts"]], use: { - // It'd be very nice to add this, but there are some tests that need - // tweaking to make it work consistently (notably, ones that wait for agent - // stats on the workspace page. The default is like 50 seconds, which is - // way too long and makes it painful to wait for test runs in CI. - // actionTimeout: 5000, // 5 seconds + actionTimeout: 5000, baseURL: `http://localhost:${coderPort}`, video: "retain-on-failure", ...(wsEndpoint diff --git a/site/e2e/tests/app.spec.ts b/site/e2e/tests/app.spec.ts index bf127ce9f21b7..9682fcb5751dc 100644 --- a/site/e2e/tests/app.spec.ts +++ b/site/e2e/tests/app.spec.ts @@ -13,6 +13,8 @@ import { beforeCoderTest } from "../hooks"; test.beforeEach(({ page }) => beforeCoderTest(page)); test("app", async ({ context, page }) => { + test.setTimeout(75_000); + const appContent = "Hello World"; const token = randomUUID(); const srv = http @@ -56,7 +58,7 @@ test("app", async ({ context, page }) => { // Wait for the web terminal to open in a new tab const pagePromise = context.waitForEvent("page"); - await page.getByText(appName).click(); + await page.getByText(appName).click({ timeout: 60_000 }); const app = await pagePromise; await app.waitForLoadState("domcontentloaded"); await app.getByText(appContent).isVisible(); diff --git a/site/e2e/tests/outdatedAgent.spec.ts b/site/e2e/tests/outdatedAgent.spec.ts index a4e42e62ec725..422074d92e341 100644 --- a/site/e2e/tests/outdatedAgent.spec.ts +++ b/site/e2e/tests/outdatedAgent.spec.ts @@ -17,7 +17,7 @@ const agentVersion = "v2.12.1"; test.beforeEach(({ page }) => beforeCoderTest(page)); test(`ssh with agent ${agentVersion}`, async ({ page }) => { - test.setTimeout(40_000); // This is a slow test, 20s may not be enough on Mac. + test.setTimeout(60_000); const token = randomUUID(); const template = await createTemplate(page, { diff --git a/site/e2e/tests/outdatedCLI.spec.ts b/site/e2e/tests/outdatedCLI.spec.ts index 22301483e0977..3470367c63546 100644 --- a/site/e2e/tests/outdatedCLI.spec.ts +++ b/site/e2e/tests/outdatedCLI.spec.ts @@ -17,6 +17,8 @@ const clientVersion = "v2.8.0"; test.beforeEach(({ page }) => beforeCoderTest(page)); test(`ssh with client ${clientVersion}`, async ({ page }) => { + test.setTimeout(60_000); + const token = randomUUID(); const template = await createTemplate(page, { apply: [ diff --git a/site/e2e/tests/webTerminal.spec.ts b/site/e2e/tests/webTerminal.spec.ts index 6db4363a4e360..fc6baec7daa67 100644 --- a/site/e2e/tests/webTerminal.spec.ts +++ b/site/e2e/tests/webTerminal.spec.ts @@ -12,6 +12,8 @@ import { beforeCoderTest } from "../hooks"; test.beforeEach(({ page }) => beforeCoderTest(page)); test("web terminal", async ({ context, page }) => { + test.setTimeout(75_000); + const token = randomUUID(); const template = await createTemplate(page, { apply: [ From e9fbfcc45b996e7e55f1f7a06cdedeb595b4f717 Mon Sep 17 00:00:00 2001 From: Stephen Kirby <58410745+stirby@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:41:52 -0500 Subject: [PATCH 009/223] chore(docs): include custom roles examples and mention of password reset (#15294) Added example custom roles for admin inspiration, mention of headless authentication use case, and user-activated password reset. --- docs/admin/users/groups-roles.md | 19 +++++++++++++++++++ docs/admin/users/index.md | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/admin/users/groups-roles.md b/docs/admin/users/groups-roles.md index 17c0fc8b5b8b9..e40efb0bd5a10 100644 --- a/docs/admin/users/groups-roles.md +++ b/docs/admin/users/groups-roles.md @@ -42,6 +42,25 @@ in the dashboard under **Organizations** -> **My Organization** -> **Roles**. ![Custom roles](../../images/admin/users/roles/custom-roles.PNG) +### Example roles + +- The `Banking Compliance Auditor` custom role cannot create workspaces, but can + read template source code and view audit logs +- The `Organization Lead` role can access user workspaces for troubleshooting + purposes, but cannot edit templates +- The `Platform Member` role cannot edit or create workspaces as they are + created via a third-party system + +Custom roles can also be applied to +[headless user accounts](./headless-auth.md): + +- A `Health Check` role can view deployment status but cannot create workspaces, + manage templates, or view users +- A `CI` role can update manage templates but cannot create workspaces or view + users + +### Creating custom roles + Clicking "Create custom role" opens a UI to select the desired permissions for a given persona. diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index 6b500ea68ac66..a00030a514f05 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -143,7 +143,12 @@ Confirm the user activation by typing **yes** and pressing **enter**. ## Reset a password -To reset a user's via the web UI: +As of 2.17.0, users can reset their password independently on the login screen +by clicking "Forgot Password." This feature requires +[email notifications](../monitoring/notifications/index.md#smtp-email) to be +configured on the deployment. + +To reset a user's password as an administrator via the web UI: 1. Go to **Users**. 2. Find the user whose password you want to reset, click the vertical ellipsis From 3de98c25dbec48cc161c82c842d7e988d36a213b Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 30 Oct 2024 13:41:16 -0500 Subject: [PATCH 010/223] feat: add prometheus metric for tracking user statuses (#15281) --- cli/server.go | 8 +- coderd/prometheusmetrics/prometheusmetrics.go | 56 ++++++++++- .../prometheusmetrics_test.go | 97 ++++++++++++++++++- 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/cli/server.go b/cli/server.go index d0282004a2aa1..cd0ba9d7927eb 100644 --- a/cli/server.go +++ b/cli/server.go @@ -212,10 +212,16 @@ func enablePrometheus( options.PrometheusRegistry.MustRegister(collectors.NewGoCollector()) options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0) + closeActiveUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.Logger.Named("active_user_metrics"), options.PrometheusRegistry, options.Database, 0) if err != nil { return nil, xerrors.Errorf("register active users prometheus metric: %w", err) } + afterCtx(ctx, closeActiveUsersFunc) + + closeUsersFunc, err := prometheusmetrics.Users(ctx, options.Logger.Named("user_metrics"), quartz.NewReal(), options.PrometheusRegistry, options.Database, 0) + if err != nil { + return nil, xerrors.Errorf("register users prometheus metric: %w", err) + } afterCtx(ctx, closeUsersFunc) closeWorkspacesFunc, err := prometheusmetrics.Workspaces(ctx, options.Logger.Named("workspaces_metrics"), options.PrometheusRegistry, options.Database, 0) diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index ebd50ff0f42ce..ccd88a9e3fc1d 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/xerrors" "tailscale.com/tailcfg" "cdr.dev/slog" @@ -22,12 +23,13 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" + "github.com/coder/quartz" ) const defaultRefreshRate = time.Minute // ActiveUsers tracks the number of users that have authenticated within the past hour. -func ActiveUsers(ctx context.Context, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) { +func ActiveUsers(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) { if duration == 0 { duration = defaultRefreshRate } @@ -58,6 +60,7 @@ func ActiveUsers(ctx context.Context, registerer prometheus.Registerer, db datab apiKeys, err := db.GetAPIKeysLastUsedAfter(ctx, dbtime.Now().Add(-1*time.Hour)) if err != nil { + logger.Error(ctx, "get api keys for active users prometheus metric", slog.Error(err)) continue } distinctUsers := map[uuid.UUID]struct{}{} @@ -73,6 +76,57 @@ func ActiveUsers(ctx context.Context, registerer prometheus.Registerer, db datab }, nil } +// Users tracks the total number of registered users, partitioned by status. +func Users(ctx context.Context, logger slog.Logger, clk quartz.Clock, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) { + if duration == 0 { + // It's not super important this tracks real-time. + duration = defaultRefreshRate * 5 + } + + gauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Subsystem: "api", + Name: "total_user_count", + Help: "The total number of registered users, partitioned by status.", + }, []string{"status"}) + err := registerer.Register(gauge) + if err != nil { + return nil, xerrors.Errorf("register total_user_count gauge: %w", err) + } + + ctx, cancelFunc := context.WithCancel(ctx) + done := make(chan struct{}) + ticker := clk.NewTicker(duration) + go func() { + defer close(done) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + + gauge.Reset() + //nolint:gocritic // This is a system service that needs full access + //to the users table. + users, err := db.GetUsers(dbauthz.AsSystemRestricted(ctx), database.GetUsersParams{}) + if err != nil { + logger.Error(ctx, "get all users for prometheus metrics", slog.Error(err)) + continue + } + + for _, user := range users { + gauge.WithLabelValues(string(user.Status)).Inc() + } + } + }() + return func() { + cancelFunc() + <-done + }, nil +} + // Workspaces tracks the total number of workspaces with labels on status. func Workspaces(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, duration time.Duration) (func(), error) { if duration == 0 { diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index 1c904d9f342e2..84aeda148662e 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -38,6 +38,7 @@ import ( "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/tailnettest" "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" ) func TestActiveUsers(t *testing.T) { @@ -98,7 +99,7 @@ func TestActiveUsers(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { t.Parallel() registry := prometheus.NewRegistry() - closeFunc, err := prometheusmetrics.ActiveUsers(context.Background(), registry, tc.Database(t), time.Millisecond) + closeFunc, err := prometheusmetrics.ActiveUsers(context.Background(), slogtest.Make(t, nil), registry, tc.Database(t), time.Millisecond) require.NoError(t, err) t.Cleanup(closeFunc) @@ -112,6 +113,100 @@ func TestActiveUsers(t *testing.T) { } } +func TestUsers(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + Name string + Database func(t *testing.T) database.Store + Count map[database.UserStatus]int + }{{ + Name: "None", + Database: func(t *testing.T) database.Store { + return dbmem.New() + }, + Count: map[database.UserStatus]int{}, + }, { + Name: "One", + Database: func(t *testing.T) database.Store { + db := dbmem.New() + dbgen.User(t, db, database.User{Status: database.UserStatusActive}) + return db + }, + Count: map[database.UserStatus]int{database.UserStatusActive: 1}, + }, { + Name: "MultipleStatuses", + Database: func(t *testing.T) database.Store { + db := dbmem.New() + + dbgen.User(t, db, database.User{Status: database.UserStatusActive}) + dbgen.User(t, db, database.User{Status: database.UserStatusDormant}) + + return db + }, + Count: map[database.UserStatus]int{database.UserStatusActive: 1, database.UserStatusDormant: 1}, + }, { + Name: "MultipleActive", + Database: func(t *testing.T) database.Store { + db := dbmem.New() + dbgen.User(t, db, database.User{Status: database.UserStatusActive}) + dbgen.User(t, db, database.User{Status: database.UserStatusActive}) + dbgen.User(t, db, database.User{Status: database.UserStatusActive}) + return db + }, + Count: map[database.UserStatus]int{database.UserStatusActive: 3}, + }} { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + registry := prometheus.NewRegistry() + mClock := quartz.NewMock(t) + db := tc.Database(t) + closeFunc, err := prometheusmetrics.Users(context.Background(), slogtest.Make(t, nil), mClock, registry, db, time.Millisecond) + require.NoError(t, err) + t.Cleanup(closeFunc) + + _, w := mClock.AdvanceNext() + w.MustWait(ctx) + + checkFn := func() bool { + metrics, err := registry.Gather() + if err != nil { + return false + } + + // If we get no metrics and we know none should exist, bail + // early. If we get no metrics but we expect some, retry. + if len(metrics) == 0 { + return len(tc.Count) == 0 + } + + for _, metric := range metrics[0].Metric { + if tc.Count[database.UserStatus(*metric.Label[0].Value)] != int(metric.Gauge.GetValue()) { + return false + } + } + + return true + } + + require.Eventually(t, checkFn, testutil.WaitShort, testutil.IntervalFast) + + // Add another dormant user and ensure it updates + dbgen.User(t, db, database.User{Status: database.UserStatusDormant}) + tc.Count[database.UserStatusDormant]++ + + _, w = mClock.AdvanceNext() + w.MustWait(ctx) + + require.Eventually(t, checkFn, testutil.WaitShort, testutil.IntervalFast) + }) + } +} + func TestWorkspaceLatestBuildTotals(t *testing.T) { t.Parallel() From 591cefa4286d494ecc3ed70364e7b3d9360db1ab Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 30 Oct 2024 19:16:59 +0000 Subject: [PATCH 011/223] fix(coderd/provisionerdserver): prevent NPE if no user link exists (#15289) --- coderd/provisionerdserver/provisionerdserver.go | 2 +- .../provisionerdserver_internal_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 0a4198423e403..1f97863e7802b 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2026,7 +2026,7 @@ func obtainOIDCAccessToken(ctx context.Context, db database.Store, oidcConfig pr LoginType: database.LoginTypeOIDC, }) if errors.Is(err, sql.ErrNoRows) { - err = nil + return "", nil } if err != nil { return "", xerrors.Errorf("get owner oidc link: %w", err) diff --git a/coderd/provisionerdserver/provisionerdserver_internal_test.go b/coderd/provisionerdserver/provisionerdserver_internal_test.go index acf9508307070..eb616eb4c2795 100644 --- a/coderd/provisionerdserver/provisionerdserver_internal_test.go +++ b/coderd/provisionerdserver/provisionerdserver_internal_test.go @@ -38,6 +38,16 @@ func TestObtainOIDCAccessToken(t *testing.T) { _, err := obtainOIDCAccessToken(ctx, db, &oauth2.Config{}, user.ID) require.NoError(t, err) }) + t.Run("MissingLink", func(t *testing.T) { + t.Parallel() + db := dbmem.New() + user := dbgen.User(t, db, database.User{ + LoginType: database.LoginTypeOIDC, + }) + tok, err := obtainOIDCAccessToken(ctx, db, &oauth2.Config{}, user.ID) + require.Empty(t, tok) + require.NoError(t, err) + }) t.Run("Exchange", func(t *testing.T) { t.Parallel() db := dbmem.New() From 6e54bd918387c7b591f47b82dcbd7317f39e4e95 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 31 Oct 2024 15:48:49 +0200 Subject: [PATCH 012/223] test(coderd/notifications): fix data race in tests and smpttest (#15304) --- .../notifications/dispatch/smtptest/server.go | 20 ++++++++++++++++++- coderd/notifications/notifications_test.go | 15 +++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/coderd/notifications/dispatch/smtptest/server.go b/coderd/notifications/dispatch/smtptest/server.go index 689b4d384036d..deb0d672604dc 100644 --- a/coderd/notifications/dispatch/smtptest/server.go +++ b/coderd/notifications/dispatch/smtptest/server.go @@ -5,6 +5,7 @@ import ( _ "embed" "io" "net" + "slices" "sync" "time" @@ -53,11 +54,22 @@ func (b *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) { return &Session{conn: c, backend: b}, nil } +// LastMessage returns a copy of the last message received by the +// backend. func (b *Backend) LastMessage() *Message { - return b.lastMsg + b.mu.Lock() + defer b.mu.Unlock() + if b.lastMsg == nil { + return nil + } + clone := *b.lastMsg + clone.To = slices.Clone(b.lastMsg.To) + return &clone } func (b *Backend) Reset() { + b.mu.Lock() + defer b.mu.Unlock() b.lastMsg = nil } @@ -84,6 +96,9 @@ func (s *Session) Auth(mech string) (sasl.Server, error) { switch mech { case sasl.Plain: return sasl.NewPlainServer(func(identity, username, password string) error { + s.backend.mu.Lock() + defer s.backend.mu.Unlock() + s.backend.lastMsg.Identity = identity s.backend.lastMsg.Username = username s.backend.lastMsg.Password = password @@ -102,6 +117,9 @@ func (s *Session) Auth(mech string) (sasl.Server, error) { }), nil case sasl.Login: return sasl.NewLoginServer(func(username, password string) error { + s.backend.mu.Lock() + defer s.backend.mu.Unlock() + s.backend.lastMsg.Username = username s.backend.lastMsg.Password = password diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 86ed14fe90957..dacf4a1f7156c 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1253,12 +1253,12 @@ func TestNotificationTemplates_Golden(t *testing.T) { // Spin up the mock webhook server var body []byte var readErr error - var webhookReceived bool + webhookReceived := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) body, readErr = io.ReadAll(r.Body) - webhookReceived = true + close(webhookReceived) })) t.Cleanup(server.Close) @@ -1302,12 +1302,11 @@ func TestNotificationTemplates_Golden(t *testing.T) { ) require.NoError(t, err) - require.Eventually(t, func() bool { - return webhookReceived - }, testutil.WaitShort, testutil.IntervalFast) - - require.NoError(t, err) - + select { + case <-time.After(testutil.WaitShort): + require.Fail(t, "timed out waiting for webhook to be received") + case <-webhookReceived: + } // Handle the body that was read in the http server here. // We need to do it here because we can't call require.* in a separate goroutine, such as the http server handler require.NoError(t, readErr) From 4849b4d8ac32c4df2b0442f7fc62646814a18e98 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 31 Oct 2024 12:26:30 -0300 Subject: [PATCH 013/223] refactor(site): refactor DAU chart to avoid seat consumption focus (#15307) Related to [https://github.com/coder/coder/issues/15297](https://github.com/coder/coder/issues/15297#issuecomment-2450052538) - Clearly display this as Daily Active Users - Remove the user limit bar at the top for licensed deployments - Explain in the tooltip that this is for measuring user activity and has no connection to license consumption --- .../ActiveUserChart.stories.tsx | 6 --- .../ActiveUserChart/ActiveUserChart.tsx | 49 ++++--------------- .../GeneralSettingsPageView.stories.tsx | 7 --- .../GeneralSettingsPageView.tsx | 12 +---- .../TemplateInsightsPage.stories.tsx | 8 --- .../TemplateInsightsPage.tsx | 3 +- 6 files changed, 12 insertions(+), 73 deletions(-) diff --git a/site/src/components/ActiveUserChart/ActiveUserChart.stories.tsx b/site/src/components/ActiveUserChart/ActiveUserChart.stories.tsx index d8735d3f5cf71..4f28d7243a0bf 100644 --- a/site/src/components/ActiveUserChart/ActiveUserChart.stories.tsx +++ b/site/src/components/ActiveUserChart/ActiveUserChart.stories.tsx @@ -22,9 +22,3 @@ export default meta; type Story = StoryObj; export const Example: Story = {}; - -export const UserLimit: Story = { - args: { - userLimit: 10, - }, -}; diff --git a/site/src/components/ActiveUserChart/ActiveUserChart.tsx b/site/src/components/ActiveUserChart/ActiveUserChart.tsx index f1695b0641cc5..41345ea8f03f8 100644 --- a/site/src/components/ActiveUserChart/ActiveUserChart.tsx +++ b/site/src/components/ActiveUserChart/ActiveUserChart.tsx @@ -14,7 +14,6 @@ import { Tooltip, defaults, } from "chart.js"; -import annotationPlugin from "chartjs-plugin-annotation"; import { HelpTooltip, HelpTooltipContent, @@ -36,21 +35,16 @@ ChartJS.register( Title, Tooltip, Legend, - annotationPlugin, ); -const USER_LIMIT_DISPLAY_THRESHOLD = 60; - export interface ActiveUserChartProps { data: readonly { date: string; amount: number }[]; interval: "day" | "week"; - userLimit: number | undefined; } export const ActiveUserChart: FC = ({ data, interval, - userLimit, }) => { const theme = useTheme(); @@ -64,24 +58,6 @@ export const ActiveUserChart: FC = ({ responsive: true, animation: false, plugins: { - annotation: { - annotations: [ - { - type: "line", - scaleID: "y", - display: shouldDisplayUserLimit(userLimit, chartData), - value: userLimit, - borderColor: theme.palette.secondary.contrastText, - borderWidth: 5, - label: { - content: "User limit", - color: theme.palette.primary.contrastText, - display: true, - font: { weight: "normal" }, - }, - }, - ], - }, legend: { display: false, }, @@ -103,7 +79,6 @@ export const ActiveUserChart: FC = ({ precision: 0, }, }, - x: { grid: { color: theme.palette.divider }, ticks: { @@ -138,32 +113,26 @@ export const ActiveUserChart: FC = ({ ); }; -export const ActiveUsersTitle: FC = () => { +type ActiveUsersTitleProps = { + interval: "day" | "week"; +}; + +export const ActiveUsersTitle: FC = ({ interval }) => { return (
- Active Users + {interval === "day" ? "Daily" : "Weekly"} Active Users How do we calculate active users? When a connection is initiated to a user's workspace they are - considered an active user. e.g. apps, web terminal, SSH + considered an active user. e.g. apps, web terminal, SSH. This is for + measuring user activity and has no connection to license + consumption.
); }; - -function shouldDisplayUserLimit( - userLimit: number | undefined, - activeUsers: number[], -): boolean { - if (!userLimit || activeUsers.length === 0) { - return false; - } - return ( - Math.max(...activeUsers) >= (userLimit * USER_LIMIT_DISPLAY_THRESHOLD) / 100 - ); -} diff --git a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx index 6f09110e77a5e..9147a1a5befff 100644 --- a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx @@ -50,13 +50,6 @@ type Story = StoryObj; export const Page: Story = {}; -export const WithUserLimit: Story = { - args: { - deploymentDAUs: MockDeploymentDAUResponse, - entitlements: MockEntitlementsWithUserLimit, - }, -}; - export const NoDAUs: Story = { args: { deploymentDAUs: undefined, diff --git a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index 0b4ee0c6d0c43..29edacd08d9e7 100644 --- a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -49,16 +49,8 @@ export const GeneralSettingsPageView: FC = ({ )} {deploymentDAUs && (
- }> - + }> +
)} diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 7fe492a1a3275..5ab6c0ea259f4 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -868,11 +868,3 @@ export const Loaded: Story = { }, }, }; - -export const LoadedWithUserLimit: Story = { - ...Loaded, - args: { - ...Loaded.args, - entitlements: MockEntitlementsWithUserLimit, - }, -}; diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index a7e0351e8ba80..f205194a1aded 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -249,7 +249,7 @@ const ActiveUsersPanel: FC = ({ - + @@ -258,7 +258,6 @@ const ActiveUsersPanel: FC = ({ {data && data.length > 0 && ( ({ amount: d.active_users, date: d.start_time, From 9d03e0429fc572d18010226c37974330aa3d640b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 31 Oct 2024 12:33:07 -0400 Subject: [PATCH 014/223] fix: workspaces query to correctly user username from users table (#15305) The subquery on the users table was incorrectly using the username from the `workspaces` table, not the `users` table. This passed `sqlc-vet` because the column did exist in the query, it just was not the correct one. --- coderd/database/queries.sql.go | 2 +- coderd/database/queries/workspaces.sql | 2 +- coderd/workspaces_test.go | 33 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d00c4ec3bcdef..38b174272522f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14947,7 +14947,7 @@ WHERE -- Filter by owner_name AND CASE WHEN $8 :: text != '' THEN - workspaces.owner_id = (SELECT id FROM users WHERE lower(owner_username) = lower($8) AND deleted = false) + workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower($8) AND deleted = false) ELSE true END -- Filter by template_name diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 08e795d7a2402..369333a5eab9d 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -233,7 +233,7 @@ WHERE -- Filter by owner_name AND CASE WHEN @owner_username :: text != '' THEN - workspaces.owner_id = (SELECT id FROM users WHERE lower(owner_username) = lower(@owner_username) AND deleted = false) + workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower(@owner_username) AND deleted = false) ELSE true END -- Filter by template_name diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index c24afc67de8ba..0a4e10670132c 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1313,6 +1313,39 @@ func TestWorkspaceFilterManual(t *testing.T) { require.NoError(t, err) require.Len(t, res.Workspaces, 0) }) + t.Run("Owner", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + otherUser, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner()) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Add a non-matching workspace + coderdtest.CreateWorkspace(t, otherUser, template.ID) + + workspaces := []codersdk.Workspace{ + coderdtest.CreateWorkspace(t, client, template.ID), + coderdtest.CreateWorkspace(t, client, template.ID), + } + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + sdkUser, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + // match owner name + res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + FilterQuery: fmt.Sprintf("owner:%s", sdkUser.Username), + }) + require.NoError(t, err) + require.Len(t, res.Workspaces, len(workspaces)) + for _, found := range res.Workspaces { + require.Equal(t, found.OwnerName, sdkUser.Username) + } + }) t.Run("IDs", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) From 330acd127071eaf0aec50cd72d51abffdff98bbb Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 31 Oct 2024 17:01:51 +0000 Subject: [PATCH 015/223] chore: create ResourceNotificationMessage and AsNotifier (#15301) Closes https://github.com/coder/coder/issues/15213 This PR enables sending notifications without requiring the auth system context, instead using a new auth notifier context. --- cli/server.go | 4 +-- coderd/apidoc/docs.go | 2 ++ coderd/apidoc/swagger.json | 2 ++ coderd/database/dbauthz/dbauthz.go | 38 ++++++++++++++++++---- coderd/database/dbauthz/dbauthz_test.go | 33 ++++++++----------- coderd/notifications/notifications_test.go | 36 ++++++++++---------- coderd/rbac/object_gen.go | 11 +++++++ coderd/rbac/policy/policy.go | 8 +++++ coderd/rbac/roles_test.go | 15 +++++++++ coderd/templates.go | 4 +-- coderd/userauth.go | 4 +-- codersdk/rbacresources_gen.go | 2 ++ docs/reference/api/members.md | 5 +++ docs/reference/api/schemas.md | 1 + site/src/api/rbacresourcesGenerated.ts | 6 ++++ site/src/api/typesGenerated.ts | 4 +-- 16 files changed, 123 insertions(+), 52 deletions(-) diff --git a/cli/server.go b/cli/server.go index cd0ba9d7927eb..c053d8dc7ef02 100644 --- a/cli/server.go +++ b/cli/server.go @@ -916,8 +916,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("failed to instantiate notification manager: %w", err) } - // nolint:gocritic // TODO: create own role. - notificationsManager.Run(dbauthz.AsSystemRestricted(ctx)) + // nolint:gocritic // We need to run the manager in a notifier context. + notificationsManager.Run(dbauthz.AsNotifier(ctx)) // Run report generator to distribute periodic reports. notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal()) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 83d1fdc2c492a..750011cbbd1e1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12265,6 +12265,7 @@ const docTemplate = `{ "group_member", "idpsync_settings", "license", + "notification_message", "notification_preference", "notification_template", "oauth2_app", @@ -12298,6 +12299,7 @@ const docTemplate = `{ "ResourceGroupMember", "ResourceIdpsyncSettings", "ResourceLicense", + "ResourceNotificationMessage", "ResourceNotificationPreference", "ResourceNotificationTemplate", "ResourceOauth2App", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 9861e195b7a69..7849778302ea5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11073,6 +11073,7 @@ "group_member", "idpsync_settings", "license", + "notification_message", "notification_preference", "notification_template", "oauth2_app", @@ -11106,6 +11107,7 @@ "ResourceGroupMember", "ResourceIdpsyncSettings", "ResourceLicense", + "ResourceNotificationMessage", "ResourceNotificationPreference", "ResourceNotificationTemplate", "ResourceOauth2App", diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index ae6b307b3e7d3..76d78754255ca 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -264,6 +264,23 @@ var ( Scope: rbac.ScopeAll, }.WithCachedASTValue() + subjectNotifier = rbac.Subject{ + FriendlyName: "Notifier", + ID: uuid.Nil.String(), + Roles: rbac.Roles([]rbac.Role{ + { + Identifier: rbac.RoleIdentifier{Name: "notifier"}, + DisplayName: "Notifier", + Site: rbac.Permissions(map[string][]policy.Action{ + rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + }), + Org: map[string][]rbac.Permission{}, + User: []rbac.Permission{}, + }, + }), + Scope: rbac.ScopeAll, + }.WithCachedASTValue() + subjectSystemRestricted = rbac.Subject{ FriendlyName: "System", ID: uuid.Nil.String(), @@ -287,6 +304,7 @@ var ( rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH}, rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceCryptoKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, @@ -327,6 +345,12 @@ func AsKeyReader(ctx context.Context) context.Context { return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyReader) } +// AsNotifier returns a context with an actor that has permissions required for +// creating/reading/updating/deleting notifications. +func AsNotifier(ctx context.Context) context.Context { + return context.WithValue(ctx, authContextKey{}, subjectNotifier) +} + // AsSystemRestricted returns a context with an actor that has permissions // required for various system operations (login, logout, metrics cache). func AsSystemRestricted(ctx context.Context) context.Context { @@ -950,7 +974,7 @@ func (q *querier) AcquireLock(ctx context.Context, id int64) error { } func (q *querier) AcquireNotificationMessages(ctx context.Context, arg database.AcquireNotificationMessagesParams) ([]database.AcquireNotificationMessagesRow, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil { return nil, err } return q.db.AcquireNotificationMessages(ctx, arg) @@ -1001,14 +1025,14 @@ func (q *querier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg databa } func (q *querier) BulkMarkNotificationMessagesFailed(ctx context.Context, arg database.BulkMarkNotificationMessagesFailedParams) (int64, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil { return 0, err } return q.db.BulkMarkNotificationMessagesFailed(ctx, arg) } func (q *querier) BulkMarkNotificationMessagesSent(ctx context.Context, arg database.BulkMarkNotificationMessagesSentParams) (int64, error) { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil { return 0, err } return q.db.BulkMarkNotificationMessagesSent(ctx, arg) @@ -1185,7 +1209,7 @@ func (q *querier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Contex } func (q *querier) DeleteOldNotificationMessages(ctx context.Context) error { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceNotificationMessage); err != nil { return err } return q.db.DeleteOldNotificationMessages(ctx) @@ -1307,7 +1331,7 @@ func (q *querier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, } func (q *querier) EnqueueNotificationMessage(ctx context.Context, arg database.EnqueueNotificationMessageParams) error { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceNotificationMessage); err != nil { return err } return q.db.EnqueueNotificationMessage(ctx, arg) @@ -1321,7 +1345,7 @@ func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error { } func (q *querier) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil { return database.FetchNewMessageMetadataRow{}, err } return q.db.FetchNewMessageMetadata(ctx, arg) @@ -1686,7 +1710,7 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) { } func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg database.GetNotificationMessagesByStatusParams) ([]database.NotificationMessage, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil { return nil, err } return q.db.GetNotificationMessagesByStatus(ctx, arg) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 439cf1bdaec19..73403a95b7859 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2888,40 +2888,33 @@ func (s *MethodTestSuite) TestSystemFunctions() { func (s *MethodTestSuite) TestNotifications() { // System functions - s.Run("AcquireNotificationMessages", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications - check.Args(database.AcquireNotificationMessagesParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + s.Run("AcquireNotificationMessages", s.Subtest(func(_ database.Store, check *expects) { + check.Args(database.AcquireNotificationMessagesParams{}).Asserts(rbac.ResourceNotificationMessage, policy.ActionUpdate) })) - s.Run("BulkMarkNotificationMessagesFailed", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications - check.Args(database.BulkMarkNotificationMessagesFailedParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + s.Run("BulkMarkNotificationMessagesFailed", s.Subtest(func(_ database.Store, check *expects) { + check.Args(database.BulkMarkNotificationMessagesFailedParams{}).Asserts(rbac.ResourceNotificationMessage, policy.ActionUpdate) })) - s.Run("BulkMarkNotificationMessagesSent", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications - check.Args(database.BulkMarkNotificationMessagesSentParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + s.Run("BulkMarkNotificationMessagesSent", s.Subtest(func(_ database.Store, check *expects) { + check.Args(database.BulkMarkNotificationMessagesSentParams{}).Asserts(rbac.ResourceNotificationMessage, policy.ActionUpdate) })) - s.Run("DeleteOldNotificationMessages", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications - check.Args().Asserts(rbac.ResourceSystem, policy.ActionDelete) + s.Run("DeleteOldNotificationMessages", s.Subtest(func(_ database.Store, check *expects) { + check.Args().Asserts(rbac.ResourceNotificationMessage, policy.ActionDelete) })) - s.Run("EnqueueNotificationMessage", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications + s.Run("EnqueueNotificationMessage", s.Subtest(func(_ database.Store, check *expects) { check.Args(database.EnqueueNotificationMessageParams{ Method: database.NotificationMethodWebhook, Payload: []byte("{}"), - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + }).Asserts(rbac.ResourceNotificationMessage, policy.ActionCreate) })) s.Run("FetchNewMessageMetadata", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications u := dbgen.User(s.T(), db, database.User{}) - check.Args(database.FetchNewMessageMetadataParams{UserID: u.ID}).Asserts(rbac.ResourceSystem, policy.ActionRead) + check.Args(database.FetchNewMessageMetadataParams{UserID: u.ID}).Asserts(rbac.ResourceNotificationMessage, policy.ActionRead) })) - s.Run("GetNotificationMessagesByStatus", s.Subtest(func(db database.Store, check *expects) { - // TODO: update this test once we have a specific role for notifications + s.Run("GetNotificationMessagesByStatus", s.Subtest(func(_ database.Store, check *expects) { check.Args(database.GetNotificationMessagesByStatusParams{ Status: database.NotificationMessageStatusLeased, Limit: 10, - }).Asserts(rbac.ResourceSystem, policy.ActionRead) + }).Asserts(rbac.ResourceNotificationMessage, policy.ActionRead) })) // Notification templates diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index dacf4a1f7156c..763046bc20cb0 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -71,7 +71,7 @@ func TestBasicNotificationRoundtrip(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) method := database.NotificationMethodSmtp @@ -135,7 +135,7 @@ func TestSMTPDispatch(t *testing.T) { // SETUP // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -197,7 +197,7 @@ func TestWebhookDispatch(t *testing.T) { // SETUP // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -281,7 +281,7 @@ func TestBackpressure(t *testing.T) { store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitShort)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitShort)) const method = database.NotificationMethodWebhook cfg := defaultNotificationsConfig(method) @@ -407,7 +407,7 @@ func TestRetries(t *testing.T) { const maxAttempts = 3 // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -501,7 +501,7 @@ func TestExpiredLeaseIsRequeued(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -521,7 +521,7 @@ func TestExpiredLeaseIsRequeued(t *testing.T) { noopInterceptor := newNoopStoreSyncer(store) // nolint:gocritic // Unit test. - mgrCtx, cancelManagerCtx := context.WithCancel(dbauthz.AsSystemRestricted(context.Background())) + mgrCtx, cancelManagerCtx := context.WithCancel(dbauthz.AsNotifier(context.Background())) t.Cleanup(cancelManagerCtx) mgr, err := notifications.NewManager(cfg, noopInterceptor, defaultHelpers(), createMetrics(), logger.Named("manager")) @@ -626,7 +626,7 @@ func TestNotifierPaused(t *testing.T) { // Setup. // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -1081,7 +1081,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { }() // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) // smtp config shared between client and server smtpConfig := codersdk.NotificationsEmailConfig{ @@ -1160,12 +1160,14 @@ func TestNotificationTemplates_Golden(t *testing.T) { // as appearance changes are enterprise features and we do not want to mix those // can't use the api if tc.appName != "" { - err = (*db).UpsertApplicationName(ctx, "Custom Application") + // nolint:gocritic // Unit test. + err = (*db).UpsertApplicationName(dbauthz.AsSystemRestricted(ctx), "Custom Application") require.NoError(t, err) } if tc.logoURL != "" { - err = (*db).UpsertLogoURL(ctx, "https://custom.application/logo.png") + // nolint:gocritic // Unit test. + err = (*db).UpsertLogoURL(dbauthz.AsSystemRestricted(ctx), "https://custom.application/logo.png") require.NoError(t, err) } @@ -1248,7 +1250,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { }() // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) // Spin up the mock webhook server var body []byte @@ -1376,7 +1378,7 @@ func TestDisabledBeforeEnqueue(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -1412,7 +1414,7 @@ func TestDisabledAfterEnqueue(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -1469,7 +1471,7 @@ func TestCustomNotificationMethod(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) @@ -1573,7 +1575,7 @@ func TestNotificationsTemplates(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) api := coderdtest.New(t, createOpts(t)) // GIVEN: the first user (owner) and a regular member @@ -1610,7 +1612,7 @@ func TestNotificationDuplicates(t *testing.T) { } // nolint:gocritic // Unit test. - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong)) + ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong)) store, _ := dbtestutil.NewDB(t) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index efe798d4ae4ac..61caf0945b245 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -129,6 +129,16 @@ var ( Type: "license", } + // ResourceNotificationMessage + // Valid Actions + // - "ActionCreate" :: create notification messages + // - "ActionDelete" :: delete notification messages + // - "ActionRead" :: read notification messages + // - "ActionUpdate" :: update notification messages + ResourceNotificationMessage = Object{ + Type: "notification_message", + } + // ResourceNotificationPreference // Valid Actions // - "ActionRead" :: read notification preferences @@ -318,6 +328,7 @@ func AllResources() []Objecter { ResourceGroupMember, ResourceIdpsyncSettings, ResourceLicense, + ResourceNotificationMessage, ResourceNotificationPreference, ResourceNotificationTemplate, ResourceOauth2App, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index c553ac31cd6e3..d70dd69821429 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -262,6 +262,14 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionDelete: actDef(""), }, }, + "notification_message": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef("create notification messages"), + ActionRead: actDef("read notification messages"), + ActionUpdate: actDef("update notification messages"), + ActionDelete: actDef("delete notification messages"), + }, + }, "notification_template": { Actions: map[Action]ActionDefinition{ ActionRead: actDef("read notification templates"), diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index c5a759f4d1da6..954b5e9788c53 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -647,6 +647,21 @@ func TestRolePermissions(t *testing.T) { }, }, }, + { + Name: "NotificationMessages", + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceNotificationMessage, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner}, + false: { + memberMe, orgMemberMe, otherOrgMember, + orgAdmin, otherOrgAdmin, + orgAuditor, otherOrgAuditor, + templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, + userAdmin, orgUserAdmin, otherOrgUserAdmin, + }, + }, + }, { // Notification preferences are currently not organization-scoped // Any owner/admin may access any users' preferences diff --git a/coderd/templates.go b/coderd/templates.go index de47b5225a973..82f805f5a09c0 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -878,8 +878,8 @@ func (api *API) notifyUsersOfTemplateDeprecation(ctx context.Context, template d for userID := range users { _, err = api.NotificationsEnqueuer.Enqueue( - //nolint:gocritic // We need the system auth context to be able to send the deprecation notification. - dbauthz.AsSystemRestricted(ctx), + //nolint:gocritic // We need the notifier auth context to be able to send the deprecation notification. + dbauthz.AsNotifier(ctx), userID, notifications.TemplateTemplateDeprecated, map[string]string{ diff --git a/coderd/userauth.go b/coderd/userauth.go index 13f9b088d731f..2c83072d3b8f0 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -298,8 +298,8 @@ func (api *API) postRequestOneTimePasscode(rw http.ResponseWriter, r *http.Reque func (api *API) notifyUserRequestedOneTimePasscode(ctx context.Context, user database.User, passcode string) error { _, err := api.NotificationsEnqueuer.Enqueue( - //nolint:gocritic // We need the system auth context to be able to send the user their one-time passcode. - dbauthz.AsSystemRestricted(ctx), + //nolint:gocritic // We need the notifier auth context to be able to send the user their one-time passcode. + dbauthz.AsNotifier(ctx), user.ID, notifications.TemplateUserRequestedOneTimePasscode, map[string]string{"one_time_passcode": passcode}, diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 8c3ced0946223..c903d5f8a02ff 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -18,6 +18,7 @@ const ( ResourceGroupMember RBACResource = "group_member" ResourceIdpsyncSettings RBACResource = "idpsync_settings" ResourceLicense RBACResource = "license" + ResourceNotificationMessage RBACResource = "notification_message" ResourceNotificationPreference RBACResource = "notification_preference" ResourceNotificationTemplate RBACResource = "notification_template" ResourceOauth2App RBACResource = "oauth2_app" @@ -72,6 +73,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceGroupMember: {ActionRead}, ResourceIdpsyncSettings: {ActionRead, ActionUpdate}, ResourceLicense: {ActionCreate, ActionDelete, ActionRead}, + ResourceNotificationMessage: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceNotificationPreference: {ActionRead, ActionUpdate}, ResourceNotificationTemplate: {ActionRead, ActionUpdate}, ResourceOauth2App: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 517ac51807c06..6ac07aa21fd5d 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -193,6 +193,7 @@ Status Code **200** | `resource_type` | `group_member` | | `resource_type` | `idpsync_settings` | | `resource_type` | `license` | +| `resource_type` | `notification_message` | | `resource_type` | `notification_preference` | | `resource_type` | `notification_template` | | `resource_type` | `oauth2_app` | @@ -353,6 +354,7 @@ Status Code **200** | `resource_type` | `group_member` | | `resource_type` | `idpsync_settings` | | `resource_type` | `license` | +| `resource_type` | `notification_message` | | `resource_type` | `notification_preference` | | `resource_type` | `notification_template` | | `resource_type` | `oauth2_app` | @@ -513,6 +515,7 @@ Status Code **200** | `resource_type` | `group_member` | | `resource_type` | `idpsync_settings` | | `resource_type` | `license` | +| `resource_type` | `notification_message` | | `resource_type` | `notification_preference` | | `resource_type` | `notification_template` | | `resource_type` | `oauth2_app` | @@ -642,6 +645,7 @@ Status Code **200** | `resource_type` | `group_member` | | `resource_type` | `idpsync_settings` | | `resource_type` | `license` | +| `resource_type` | `notification_message` | | `resource_type` | `notification_preference` | | `resource_type` | `notification_template` | | `resource_type` | `oauth2_app` | @@ -901,6 +905,7 @@ Status Code **200** | `resource_type` | `group_member` | | `resource_type` | `idpsync_settings` | | `resource_type` | `license` | +| `resource_type` | `notification_message` | | `resource_type` | `notification_preference` | | `resource_type` | `notification_template` | | `resource_type` | `oauth2_app` | diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index f4e683305029b..5dbb8cb5df97f 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4491,6 +4491,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `group_member` | | `idpsync_settings` | | `license` | +| `notification_message` | | `notification_preference` | | `notification_template` | | `oauth2_app` | diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 34b2ddf021ace..79a80ef593d90 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -70,6 +70,12 @@ export const RBACResourceActions: Partial< delete: "delete license", read: "read licenses", }, + notification_message: { + create: "create notification messages", + delete: "delete notification messages", + read: "read notification messages", + update: "update notification messages", + }, notification_preference: { read: "read notification preferences", update: "update notification preferences", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d687fb68ec61f..9b68a64b02521 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2198,8 +2198,8 @@ export type RBACAction = "application_connect" | "assign" | "create" | "delete" export const RBACActions: RBACAction[] = ["application_connect", "assign", "create", "delete", "read", "read_personal", "ssh", "start", "stop", "update", "update_personal", "use", "view_insights"] // From codersdk/rbacresources_gen.go -export type RBACResource = "*" | "api_key" | "assign_org_role" | "assign_role" | "audit_log" | "crypto_key" | "debug_info" | "deployment_config" | "deployment_stats" | "file" | "group" | "group_member" | "idpsync_settings" | "license" | "notification_preference" | "notification_template" | "oauth2_app" | "oauth2_app_code_token" | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" | "template" | "user" | "workspace" | "workspace_dormant" | "workspace_proxy" -export const RBACResources: RBACResource[] = ["*", "api_key", "assign_org_role", "assign_role", "audit_log", "crypto_key", "debug_info", "deployment_config", "deployment_stats", "file", "group", "group_member", "idpsync_settings", "license", "notification_preference", "notification_template", "oauth2_app", "oauth2_app_code_token", "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", "workspace_dormant", "workspace_proxy"] +export type RBACResource = "*" | "api_key" | "assign_org_role" | "assign_role" | "audit_log" | "crypto_key" | "debug_info" | "deployment_config" | "deployment_stats" | "file" | "group" | "group_member" | "idpsync_settings" | "license" | "notification_message" | "notification_preference" | "notification_template" | "oauth2_app" | "oauth2_app_code_token" | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" | "template" | "user" | "workspace" | "workspace_dormant" | "workspace_proxy" +export const RBACResources: RBACResource[] = ["*", "api_key", "assign_org_role", "assign_role", "audit_log", "crypto_key", "debug_info", "deployment_config", "deployment_stats", "file", "group", "group_member", "idpsync_settings", "license", "notification_message", "notification_preference", "notification_template", "oauth2_app", "oauth2_app_code_token", "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", "workspace_dormant", "workspace_proxy"] // From codersdk/audit.go export type ResourceType = "api_key" | "convert_login" | "custom_role" | "git_ssh_key" | "group" | "health_settings" | "license" | "notifications_settings" | "oauth2_provider_app" | "oauth2_provider_app_secret" | "organization" | "template" | "template_version" | "user" | "workspace" | "workspace_build" | "workspace_proxy" From 14565615be6b06778dfa1e09bbcee95e5bfa9270 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 31 Oct 2024 22:37:19 +0200 Subject: [PATCH 016/223] test(coderd/database/pubsub): fix data race in err assignment (#15306) --- coderd/database/pubsub/pubsub_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/pubsub/pubsub_test.go b/coderd/database/pubsub/pubsub_test.go index 6059b0cecbd97..21b4b1d54c171 100644 --- a/coderd/database/pubsub/pubsub_test.go +++ b/coderd/database/pubsub/pubsub_test.go @@ -58,7 +58,7 @@ func TestPGPubsub_Metrics(t *testing.T) { require.NoError(t, err) defer unsub0() go func() { - err = uut.Publish(event, []byte(data)) + err := uut.Publish(event, []byte(data)) assert.NoError(t, err) }() _ = testutil.RequireRecvCtx(ctx, t, messageChannel) @@ -93,7 +93,7 @@ func TestPGPubsub_Metrics(t *testing.T) { require.NoError(t, err) defer unsub1() go func() { - err = uut.Publish(event, colossalData) + err := uut.Publish(event, colossalData) assert.NoError(t, err) }() // should get 2 messages because we have 2 subs From 088f21965bee683bc7559b93761adaad156b012d Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 31 Oct 2024 17:55:42 -0500 Subject: [PATCH 017/223] feat: add audit logs for dormancy events (#15298) --- cli/server_createadminuser.go | 1 + coderd/apidoc/docs.go | 8 ++ coderd/apidoc/swagger.json | 8 ++ coderd/audit/fields.go | 33 ++++++++ coderd/audit/request.go | 13 +-- coderd/coderd.go | 1 + coderd/coderdtest/coderdtest.go | 3 + coderd/database/dbgen/dbgen.go | 1 + coderd/database/dbmem/dbmem.go | 8 +- coderd/database/queries.sql.go | 21 ++++- coderd/database/queries/users.sql | 11 ++- coderd/httpmw/apikey.go | 18 ++-- .../provisionerdserver/provisionerdserver.go | 1 + coderd/userauth.go | 84 +++++++++++++++---- coderd/userauth_test.go | 45 +++++++++- coderd/users.go | 17 +++- coderd/users_test.go | 36 ++++++++ codersdk/users.go | 2 + docs/reference/api/schemas.md | 18 ++-- docs/reference/api/users.md | 1 + enterprise/cli/server.go | 3 +- enterprise/coderd/coderd.go | 1 + enterprise/coderd/dormancy/dormantusersjob.go | 64 +++++++------- .../coderd/dormancy/dormantusersjob_test.go | 44 +++++----- site/src/api/typesGenerated.ts | 1 + .../AuditLogDescription.tsx | 2 +- 26 files changed, 340 insertions(+), 105 deletions(-) create mode 100644 coderd/audit/fields.go diff --git a/cli/server_createadminuser.go b/cli/server_createadminuser.go index 0619688468554..7ef95e7e093e6 100644 --- a/cli/server_createadminuser.go +++ b/cli/server_createadminuser.go @@ -197,6 +197,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command { UpdatedAt: dbtime.Now(), RBACRoles: []string{rbac.RoleOwner().String()}, LoginType: database.LoginTypePassword, + Status: "", }) if err != nil { return xerrors.Errorf("insert user: %w", err) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 750011cbbd1e1..372303c320a34 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9896,6 +9896,14 @@ const docTemplate = `{ "password": { "type": "string" }, + "user_status": { + "description": "UserStatus defaults to UserStatusDormant.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.UserStatus" + } + ] + }, "username": { "type": "string" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7849778302ea5..db8b53e966bf4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8809,6 +8809,14 @@ "password": { "type": "string" }, + "user_status": { + "description": "UserStatus defaults to UserStatusDormant.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.UserStatus" + } + ] + }, "username": { "type": "string" } diff --git a/coderd/audit/fields.go b/coderd/audit/fields.go new file mode 100644 index 0000000000000..db0879730425a --- /dev/null +++ b/coderd/audit/fields.go @@ -0,0 +1,33 @@ +package audit + +import ( + "context" + "encoding/json" + + "cdr.dev/slog" +) + +type BackgroundSubsystem string + +const ( + BackgroundSubsystemDormancy BackgroundSubsystem = "dormancy" +) + +func BackgroundTaskFields(subsystem BackgroundSubsystem) map[string]string { + return map[string]string{ + "automatic_actor": "coder", + "automatic_subsystem": string(subsystem), + } +} + +func BackgroundTaskFieldsBytes(ctx context.Context, logger slog.Logger, subsystem BackgroundSubsystem) []byte { + af := BackgroundTaskFields(subsystem) + + wriBytes, err := json.Marshal(af) + if err != nil { + logger.Error(ctx, "marshal additional fields for dormancy audit", slog.Error(err)) + return []byte("{}") + } + + return wriBytes +} diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 88b637384eeda..c8b7bf17b4b96 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -62,12 +62,13 @@ type BackgroundAuditParams[T Auditable] struct { Audit Auditor Log slog.Logger - UserID uuid.UUID - RequestID uuid.UUID - Status int - Action database.AuditAction - OrganizationID uuid.UUID - IP string + UserID uuid.UUID + RequestID uuid.UUID + Status int + Action database.AuditAction + OrganizationID uuid.UUID + IP string + // todo: this should automatically marshal an interface{} instead of accepting a raw message. AdditionalFields json.RawMessage New T diff --git a/coderd/coderd.go b/coderd/coderd.go index bd844d7ca13c3..70101b7020890 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -702,6 +702,7 @@ func New(options *Options) *API { apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ DB: options.Database, + ActivateDormantUser: ActivateDormantUser(options.Logger, &api.Auditor, options.Database), OAuth2Configs: oauthConfigs, RedirectToLogin: false, DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(), diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index e287e04b8d0cf..f3868bf14d54b 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -719,6 +719,9 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI Name: RandomName(t), Password: "SomeSecurePassword!", OrganizationIDs: organizationIDs, + // Always create users as active in tests to ignore an extra audit log + // when logging in. + UserStatus: ptr.Ref(codersdk.UserStatusActive), } for _, m := range mutators { m(&req) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 69419b98c79b1..3df873cdb4cbf 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -342,6 +342,7 @@ func User(t testing.TB, db database.Store, orig database.User) database.User { UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), RBACRoles: takeFirstSlice(orig.RBACRoles, []string{}), LoginType: takeFirst(orig.LoginType, database.LoginTypePassword), + Status: string(takeFirst(orig.Status, database.UserStatusDormant)), }) require.NoError(t, err, "insert user") diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 4f54598744dd0..e949b5be4880d 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7709,6 +7709,11 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam } } + status := database.UserStatusDormant + if arg.Status != "" { + status = database.UserStatus(arg.Status) + } + user := database.User{ ID: arg.ID, Email: arg.Email, @@ -7717,7 +7722,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam UpdatedAt: arg.UpdatedAt, Username: arg.Username, Name: arg.Name, - Status: database.UserStatusDormant, + Status: status, RBACRoles: arg.RBACRoles, LoginType: arg.LoginType, } @@ -8640,6 +8645,7 @@ func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params dat updated = append(updated, database.UpdateInactiveUsersToDormantRow{ ID: user.ID, Email: user.Email, + Username: user.Username, LastSeenAt: user.LastSeenAt, }) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 38b174272522f..46928ae1d3738 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -10345,10 +10345,15 @@ INSERT INTO created_at, updated_at, rbac_roles, - login_type + login_type, + status ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at + ($1, $2, $3, $4, $5, $6, $7, $8, $9, + -- if the status passed in is empty, fallback to dormant, which is what + -- we were doing before. + COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status) + ) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at ` type InsertUserParams struct { @@ -10361,6 +10366,7 @@ type InsertUserParams struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"` LoginType LoginType `db:"login_type" json:"login_type"` + Status string `db:"status" json:"status"` } func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) { @@ -10374,6 +10380,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User arg.UpdatedAt, arg.RBACRoles, arg.LoginType, + arg.Status, ) var i User err := row.Scan( @@ -10408,7 +10415,7 @@ SET WHERE last_seen_at < $2 :: timestamp AND status = 'active'::user_status -RETURNING id, email, last_seen_at +RETURNING id, email, username, last_seen_at ` type UpdateInactiveUsersToDormantParams struct { @@ -10419,6 +10426,7 @@ type UpdateInactiveUsersToDormantParams struct { type UpdateInactiveUsersToDormantRow struct { ID uuid.UUID `db:"id" json:"id"` Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"` } @@ -10431,7 +10439,12 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat var items []UpdateInactiveUsersToDormantRow for rows.Next() { var i UpdateInactiveUsersToDormantRow - if err := rows.Scan(&i.ID, &i.Email, &i.LastSeenAt); err != nil { + if err := rows.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.LastSeenAt, + ); err != nil { return nil, err } items = append(items, i) diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 013e2b8027a45..a4f8844fd2db5 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -67,10 +67,15 @@ INSERT INTO created_at, updated_at, rbac_roles, - login_type + login_type, + status ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, + -- if the status passed in is empty, fallback to dormant, which is what + -- we were doing before. + COALESCE(NULLIF(@status::text, '')::user_status, 'dormant'::user_status) + ) RETURNING *; -- name: UpdateUserProfile :one UPDATE @@ -286,7 +291,7 @@ SET WHERE last_seen_at < @last_seen_after :: timestamp AND status = 'active'::user_status -RETURNING id, email, last_seen_at; +RETURNING id, email, username, last_seen_at; -- AllUserIDs returns all UserIDs regardless of user status or deletion. -- name: AllUserIDs :many diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index c4d1c7f202533..f6746b95eb20e 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -82,6 +82,7 @@ const ( type ExtractAPIKeyConfig struct { DB database.Store + ActivateDormantUser func(ctx context.Context, u database.User) (database.User, error) OAuth2Configs *OAuth2Configs RedirectToLogin bool DisableSessionExpiryRefresh bool @@ -414,21 +415,20 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon }) } - if userStatus == database.UserStatusDormant { - // If coder confirms that the dormant user is valid, it can switch their account to active. - // nolint:gocritic - u, err := cfg.DB.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{ - ID: key.UserID, - Status: database.UserStatusActive, - UpdatedAt: dbtime.Now(), + if userStatus == database.UserStatusDormant && cfg.ActivateDormantUser != nil { + id, _ := uuid.Parse(actor.ID) + user, err := cfg.ActivateDormantUser(ctx, database.User{ + ID: id, + Username: actor.FriendlyName, + Status: userStatus, }) if err != nil { return write(http.StatusInternalServerError, codersdk.Response{ Message: internalErrorMessage, - Detail: fmt.Sprintf("can't activate a dormant user: %s", err.Error()), + Detail: fmt.Sprintf("update user status: %s", err.Error()), }) } - userStatus = u.Status + userStatus = user.Status } if userStatus != database.UserStatusActive { diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 1f97863e7802b..6c72ff5831947 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1063,6 +1063,7 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto. wriBytes, err := json.Marshal(buildResourceInfo) if err != nil { s.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err)) + wriBytes = []byte("{}") } bag := audit.BaggageFromContext(ctx) diff --git a/coderd/userauth.go b/coderd/userauth.go index 2c83072d3b8f0..317bb5b6a9e58 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -27,6 +28,7 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/apikey" "github.com/coder/coder/v2/coderd/audit" @@ -565,20 +567,13 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co return user, rbac.Subject{}, false } - if user.Status == database.UserStatusDormant { - //nolint:gocritic // System needs to update status of the user account (dormant -> active). - user, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{ - ID: user.ID, - Status: database.UserStatusActive, - UpdatedAt: dbtime.Now(), + user, err = ActivateDormantUser(api.Logger, &api.Auditor, api.Database)(ctx, user) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error.", + Detail: err.Error(), }) - if err != nil { - logger.Error(ctx, "unable to update user status to active", slog.Error(err)) - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error occurred. Try again later, or contact an admin for assistance.", - }) - return user, rbac.Subject{}, false - } + return user, rbac.Subject{}, false } subject, userStatus, err := httpmw.UserRBACSubject(ctx, api.Database, user.ID, rbac.ScopeAll) @@ -601,6 +596,42 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co return user, subject, true } +func ActivateDormantUser(logger slog.Logger, auditor *atomic.Pointer[audit.Auditor], db database.Store) func(ctx context.Context, user database.User) (database.User, error) { + return func(ctx context.Context, user database.User) (database.User, error) { + if user.ID == uuid.Nil || user.Status != database.UserStatusDormant { + return user, nil + } + + //nolint:gocritic // System needs to update status of the user account (dormant -> active). + newUser, err := db.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{ + ID: user.ID, + Status: database.UserStatusActive, + UpdatedAt: dbtime.Now(), + }) + if err != nil { + logger.Error(ctx, "unable to update user status to active", slog.Error(err)) + return user, xerrors.Errorf("update user status: %w", err) + } + + oldAuditUser := user + newAuditUser := user + newAuditUser.Status = database.UserStatusActive + + audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.User]{ + Audit: *auditor.Load(), + Log: logger, + UserID: user.ID, + Action: database.AuditActionWrite, + Old: oldAuditUser, + New: newAuditUser, + Status: http.StatusOK, + AdditionalFields: audit.BackgroundTaskFieldsBytes(ctx, logger, audit.BackgroundSubsystemDormancy), + }) + + return newUser, nil + } +} + // Clear the user's session cookie. // // @Summary Log out user @@ -1385,10 +1416,22 @@ func (p *oauthLoginParams) CommitAuditLogs() { func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.User, database.APIKey, error) { var ( - ctx = r.Context() - user database.User - cookies []*http.Cookie - logger = api.Logger.Named(userAuthLoggerName) + ctx = r.Context() + user database.User + cookies []*http.Cookie + logger = api.Logger.Named(userAuthLoggerName) + auditor = *api.Auditor.Load() + dormantConvertAudit *audit.Request[database.User] + initDormantAuditOnce = sync.OnceFunc(func() { + dormantConvertAudit = params.initAuditRequest(&audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: uuid.Nil, + AdditionalFields: audit.BackgroundTaskFields(audit.BackgroundSubsystemDormancy), + }) + }) ) var isConvertLoginType bool @@ -1490,6 +1533,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C Email: params.Email, Username: params.Username, OrganizationIDs: orgIDs, + UserStatus: ptr.Ref(codersdk.UserStatusActive), }, LoginType: params.LoginType, accountCreatorName: "oauth", @@ -1501,6 +1545,11 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C // Activate dormant user on sign-in if user.Status == database.UserStatusDormant { + // This is necessary because transactions can be retried, and we + // only want to add the audit log a single time. + initDormantAuditOnce() + dormantConvertAudit.UserID = user.ID + dormantConvertAudit.Old = user //nolint:gocritic // System needs to update status of the user account (dormant -> active). user, err = tx.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{ ID: user.ID, @@ -1511,6 +1560,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C logger.Error(ctx, "unable to update user status to active", slog.Error(err)) return xerrors.Errorf("update user status: %w", err) } + dormantConvertAudit.New = user } debugContext, err := json.Marshal(params.DebugContext) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 6386be7eb8be4..843f8ec753133 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -1285,7 +1285,7 @@ func TestUserOIDC(t *testing.T) { tc.AssertResponse(t, resp) } - ctx := testutil.Context(t, testutil.WaitLong) + ctx := testutil.Context(t, testutil.WaitShort) if tc.AssertUser != nil { user, err := client.User(ctx, "me") @@ -1300,6 +1300,49 @@ func TestUserOIDC(t *testing.T) { }) } + t.Run("OIDCDormancy", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + auditor := audit.NewMock() + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefresh(func(_ string) error { + return xerrors.New("refreshing token should never occur") + }), + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + }) + + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + Auditor: auditor, + OIDCConfig: cfg, + Logger: &logger, + }) + + user := dbgen.User(t, db, database.User{ + LoginType: database.LoginTypeOIDC, + Status: database.UserStatusDormant, + }) + auditor.ResetLogs() + + client, resp := fake.AttemptLogin(t, owner, jwt.MapClaims{ + "email": user.Email, + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + + auditor.Contains(t, database.AuditLog{ + ResourceType: database.ResourceTypeUser, + AdditionalFields: json.RawMessage(`{"automatic_actor":"coder","automatic_subsystem":"dormancy"}`), + }) + me, err := client.User(ctx, "me") + require.NoError(t, err) + + require.Equal(t, codersdk.UserStatusActive, me.Status) + }) + t.Run("OIDCConvert", func(t *testing.T) { t.Parallel() diff --git a/coderd/users.go b/coderd/users.go index 5e521da3a6004..445b44f334349 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -28,6 +28,7 @@ import ( "github.com/coder/coder/v2/coderd/searchquery" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/userpassword" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" ) @@ -188,10 +189,13 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { //nolint:gocritic // needed to create first user user, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{ CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{ - Email: createUser.Email, - Username: createUser.Username, - Name: createUser.Name, - Password: createUser.Password, + Email: createUser.Email, + Username: createUser.Username, + Name: createUser.Name, + Password: createUser.Password, + // There's no reason to create the first user as dormant, since you have + // to login immediately anyways. + UserStatus: ptr.Ref(codersdk.UserStatusActive), OrganizationIDs: []uuid.UUID{defaultOrg.ID}, }, LoginType: database.LoginTypePassword, @@ -1343,6 +1347,10 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create err := store.InTx(func(tx database.Store) error { orgRoles := make([]string, 0) + status := "" + if req.UserStatus != nil { + status = string(*req.UserStatus) + } params := database.InsertUserParams{ ID: uuid.New(), Email: req.Email, @@ -1354,6 +1362,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create // All new users are defaulted to members of the site. RBACRoles: []string{}, LoginType: req.LoginType, + Status: status, } // If a user signs up with OAuth, they can have no password! if req.Password != "" { diff --git a/coderd/users_test.go b/coderd/users_test.go index c33ca933a9d96..3c88d3e5022ac 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -30,6 +30,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" @@ -695,6 +696,41 @@ func TestPostUsers(t *testing.T) { }) require.NoError(t, err) + // User should default to dormant. + require.Equal(t, codersdk.UserStatusDormant, user.Status) + + require.Len(t, auditor.AuditLogs(), numLogs) + require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action) + require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action) + + require.Len(t, user.OrganizationIDs, 1) + assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0]) + }) + + t.Run("CreateWithStatus", func(t *testing.T) { + t.Parallel() + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor}) + numLogs := len(auditor.AuditLogs()) + + firstUser := coderdtest.CreateFirstUser(t, client) + numLogs++ // add an audit log for user create + numLogs++ // add an audit log for login + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", + UserStatus: ptr.Ref(codersdk.UserStatusActive), + }) + require.NoError(t, err) + + require.Equal(t, codersdk.UserStatusActive, user.Status) + require.Len(t, auditor.AuditLogs(), numLogs) require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action) require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action) diff --git a/codersdk/users.go b/codersdk/users.go index f57b8010f9229..546fcc99e9fbe 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -139,6 +139,8 @@ type CreateUserRequestWithOrgs struct { Password string `json:"password"` // UserLoginType defaults to LoginTypePassword. UserLoginType LoginType `json:"login_type"` + // UserStatus defaults to UserStatusDormant. + UserStatus *UserStatus `json:"user_status"` // OrganizationIDs is a list of organization IDs that the user should be a member of. OrganizationIDs []uuid.UUID `json:"organization_ids" validate:"" format:"uuid"` } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 5dbb8cb5df97f..384933e5795af 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1342,20 +1342,22 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "name": "string", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "password": "string", + "user_status": "active", "username": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------ | ---------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------- | -| `email` | string | true | | | -| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. | -| `name` | string | false | | | -| `organization_ids` | array of string | false | | Organization ids is a list of organization IDs that the user should be a member of. | -| `password` | string | false | | | -| `username` | string | true | | | +| Name | Type | Required | Restrictions | Description | +| ------------------ | ------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------- | +| `email` | string | true | | | +| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. | +| `name` | string | false | | | +| `organization_ids` | array of string | false | | Organization ids is a list of organization IDs that the user should be a member of. | +| `password` | string | false | | | +| `user_status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | User status defaults to UserStatusDormant. | +| `username` | string | true | | | ## codersdk.CreateWorkspaceBuildRequest diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 3979f5521b377..5e0ae3c239c04 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -86,6 +86,7 @@ curl -X POST http://coder-server:8080/api/v2/users \ "name": "string", "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "password": "string", + "user_status": "active", "username": "string" } ``` diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 930a3e4956257..1bf4f31a8506b 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/enterprise/trialer" "github.com/coder/coder/v2/tailnet" + "github.com/coder/quartz" "github.com/coder/serpent" agplcoderd "github.com/coder/coder/v2/coderd" @@ -95,7 +96,7 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { DefaultQuietHoursSchedule: options.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(), ProvisionerDaemonPSK: options.DeploymentValues.Provisioner.DaemonPSK.Value(), - CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, options.Database), + CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, quartz.NewReal(), options.Database, options.Auditor), } if encKeys := options.DeploymentValues.ExternalTokenEncryptionKeys.Value(); len(encKeys) != 0 { diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 7e59eb341411f..dddf619b34058 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -172,6 +172,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { } apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ DB: options.Database, + ActivateDormantUser: coderd.ActivateDormantUser(options.Logger, &api.AGPL.Auditor, options.Database), OAuth2Configs: oauthConfigs, RedirectToLogin: false, DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(), diff --git a/enterprise/coderd/dormancy/dormantusersjob.go b/enterprise/coderd/dormancy/dormantusersjob.go index 8c8e22310c031..cae442ce07507 100644 --- a/enterprise/coderd/dormancy/dormantusersjob.go +++ b/enterprise/coderd/dormancy/dormantusersjob.go @@ -3,14 +3,17 @@ package dormancy import ( "context" "database/sql" + "net/http" "time" "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/quartz" ) const ( @@ -22,50 +25,49 @@ const ( // CheckInactiveUsers function updates status of inactive users from active to dormant // using default parameters. -func CheckInactiveUsers(ctx context.Context, logger slog.Logger, db database.Store) func() { - return CheckInactiveUsersWithOptions(ctx, logger, db, jobInterval, accountDormancyPeriod) +func CheckInactiveUsers(ctx context.Context, logger slog.Logger, clk quartz.Clock, db database.Store, auditor audit.Auditor) func() { + return CheckInactiveUsersWithOptions(ctx, logger, clk, db, auditor, jobInterval, accountDormancyPeriod) } // CheckInactiveUsersWithOptions function updates status of inactive users from active to dormant // using provided parameters. -func CheckInactiveUsersWithOptions(ctx context.Context, logger slog.Logger, db database.Store, checkInterval, dormancyPeriod time.Duration) func() { +func CheckInactiveUsersWithOptions(ctx context.Context, logger slog.Logger, clk quartz.Clock, db database.Store, auditor audit.Auditor, checkInterval, dormancyPeriod time.Duration) func() { logger = logger.Named("dormancy") ctx, cancelFunc := context.WithCancel(ctx) - done := make(chan struct{}) - ticker := time.NewTicker(checkInterval) - go func() { - defer close(done) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - } + tf := clk.TickerFunc(ctx, checkInterval, func() error { + startTime := time.Now() + lastSeenAfter := dbtime.Now().Add(-dormancyPeriod) + logger.Debug(ctx, "check inactive user accounts", slog.F("dormancy_period", dormancyPeriod), slog.F("last_seen_after", lastSeenAfter)) - startTime := time.Now() - lastSeenAfter := dbtime.Now().Add(-dormancyPeriod) - logger.Debug(ctx, "check inactive user accounts", slog.F("dormancy_period", dormancyPeriod), slog.F("last_seen_after", lastSeenAfter)) + updatedUsers, err := db.UpdateInactiveUsersToDormant(ctx, database.UpdateInactiveUsersToDormantParams{ + LastSeenAfter: lastSeenAfter, + UpdatedAt: dbtime.Now(), + }) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + logger.Error(ctx, "can't mark inactive users as dormant", slog.Error(err)) + return nil + } - updatedUsers, err := db.UpdateInactiveUsersToDormant(ctx, database.UpdateInactiveUsersToDormantParams{ - LastSeenAfter: lastSeenAfter, - UpdatedAt: dbtime.Now(), + for _, u := range updatedUsers { + logger.Info(ctx, "account has been marked as dormant", slog.F("email", u.Email), slog.F("last_seen_at", u.LastSeenAt)) + audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.User]{ + Audit: auditor, + Log: logger, + UserID: u.ID, + Action: database.AuditActionWrite, + Old: database.User{ID: u.ID, Username: u.Username, Status: database.UserStatusActive}, + New: database.User{ID: u.ID, Username: u.Username, Status: database.UserStatusDormant}, + Status: http.StatusOK, + AdditionalFields: audit.BackgroundTaskFieldsBytes(ctx, logger, audit.BackgroundSubsystemDormancy), }) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - logger.Error(ctx, "can't mark inactive users as dormant", slog.Error(err)) - continue - } - - for _, u := range updatedUsers { - logger.Info(ctx, "account has been marked as dormant", slog.F("email", u.Email), slog.F("last_seen_at", u.LastSeenAt)) - } - logger.Debug(ctx, "checking user accounts is done", slog.F("num_dormant_accounts", len(updatedUsers)), slog.F("execution_time", time.Since(startTime))) } - }() + logger.Debug(ctx, "checking user accounts is done", slog.F("num_dormant_accounts", len(updatedUsers)), slog.F("execution_time", time.Since(startTime))) + return nil + }) return func() { cancelFunc() - <-done + _ = tf.Wait() } } diff --git a/enterprise/coderd/dormancy/dormantusersjob_test.go b/enterprise/coderd/dormancy/dormantusersjob_test.go index c752e84bc1d90..bb3e0b4170baf 100644 --- a/enterprise/coderd/dormancy/dormantusersjob_test.go +++ b/enterprise/coderd/dormancy/dormantusersjob_test.go @@ -10,10 +10,11 @@ import ( "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/enterprise/coderd/dormancy" - "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" ) func TestCheckInactiveUsers(t *testing.T) { @@ -42,29 +43,34 @@ func TestCheckInactiveUsers(t *testing.T) { suspendedUser2 := setupUser(ctx, t, db, "suspended-user-2@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-time.Hour)) suspendedUser3 := setupUser(ctx, t, db, "suspended-user-3@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-6*time.Hour)) + mAudit := audit.NewMock() + mClock := quartz.NewMock(t) // Run the periodic job - closeFunc := dormancy.CheckInactiveUsersWithOptions(ctx, logger, db, interval, dormancyPeriod) + closeFunc := dormancy.CheckInactiveUsersWithOptions(ctx, logger, mClock, db, mAudit, interval, dormancyPeriod) t.Cleanup(closeFunc) - var rows []database.GetUsersRow - var err error - require.Eventually(t, func() bool { - rows, err = db.GetUsers(ctx, database.GetUsersParams{}) - if err != nil { - return false - } + dur, w := mClock.AdvanceNext() + require.Equal(t, interval, dur) + w.MustWait(ctx) + + rows, err := db.GetUsers(ctx, database.GetUsersParams{}) + require.NoError(t, err) - var dormant, suspended int - for _, row := range rows { - if row.Status == database.UserStatusDormant { - dormant++ - } else if row.Status == database.UserStatusSuspended { - suspended++ - } + var dormant, suspended int + for _, row := range rows { + if row.Status == database.UserStatusDormant { + dormant++ + } else if row.Status == database.UserStatusSuspended { + suspended++ } - // 6 users in total, 3 dormant, 3 suspended - return len(rows) == 9 && dormant == 3 && suspended == 3 - }, testutil.WaitShort, testutil.IntervalMedium) + } + + // 9 users in total, 3 active, 3 dormant, 3 suspended + require.Len(t, rows, 9) + require.Equal(t, 3, dormant) + require.Equal(t, 3, suspended) + + require.Len(t, mAudit.AuditLogs(), 3) allUsers := ignoreUpdatedAt(database.ConvertUserRows(rows)) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 9b68a64b02521..8bb28637e526a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -328,6 +328,7 @@ export interface CreateUserRequestWithOrgs { readonly name: string; readonly password: string; readonly login_type: LoginType; + readonly user_status?: UserStatus; readonly organization_ids: Readonly>; } diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx index dd00129f935eb..51d4e8ec910d9 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx @@ -23,7 +23,7 @@ export const AuditLogDescription: FC = ({ target = ""; } - // This occurs when SCIM creates a user. + // This occurs when SCIM creates a user, or dormancy changes a users status. if ( auditLog.resource_type === "user" && auditLog.additional_fields?.automatic_actor === "coder" From 31506e694b1fed8eaf15051aa5992a489054652d Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:17:05 +1100 Subject: [PATCH 018/223] chore: send workspace pubsub events by owner id (#14964) We currently send empty payloads to pubsub channels of the form `workspace:` to notify listeners of updates to workspaces (such as for refreshing the workspace dashboard). To support https://github.com/coder/coder/issues/14716, we'll instead send `WorkspaceEvent` payloads to pubsub channels of the form `workspace_owner:`. This enables a listener to receive events for all workspaces owned by a user. This PR replaces the usage of the old channels without modifying any existing behaviors. ``` type WorkspaceEvent struct { Kind WorkspaceEventKind `json:"kind"` WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` // AgentID is only set for WorkspaceEventKindAgent* events // (excluding AgentTimeout) AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"` } ``` We've defined `WorkspaceEventKind`s based on how the old channel was used, but it's not yet necessary to inspect the types of any of the events, as the existing listeners are designed to fire off any of them. ``` WorkspaceEventKindStateChange WorkspaceEventKind = "state_change" WorkspaceEventKindStatsUpdate WorkspaceEventKind = "stats_update" WorkspaceEventKindMetadataUpdate WorkspaceEventKind = "mtd_update" WorkspaceEventKindAppHealthUpdate WorkspaceEventKind = "app_health" WorkspaceEventKindAgentLifecycleUpdate WorkspaceEventKind = "agt_lifecycle_update" WorkspaceEventKindAgentLogsUpdate WorkspaceEventKind = "agt_logs_update" WorkspaceEventKindAgentConnectionUpdate WorkspaceEventKind = "agt_connection_update" WorkspaceEventKindAgentLogsOverflow WorkspaceEventKind = "agt_logs_overflow" WorkspaceEventKindAgentTimeout WorkspaceEventKind = "agt_timeout" ``` --- coderd/agentapi/api.go | 72 ++++------------- coderd/agentapi/apps.go | 5 +- coderd/agentapi/apps_test.go | 7 +- coderd/agentapi/lifecycle.go | 19 ++--- coderd/agentapi/lifecycle_test.go | 77 +++++++----------- coderd/agentapi/logs.go | 7 +- coderd/agentapi/logs_test.go | 13 +-- coderd/agentapi/manifest.go | 15 ++-- coderd/agentapi/manifest_test.go | 16 ++-- coderd/agentapi/stats_test.go | 45 +++++++---- coderd/database/dbfake/dbfake.go | 9 ++- .../provisionerdserver/provisionerdserver.go | 47 ++++++++--- .../provisionerdserver_test.go | 81 +++++++++++++------ coderd/workspaceagents.go | 58 +++++++------ coderd/workspaceagentsrpc.go | 34 ++++++-- coderd/workspaceagentsrpc_internal_test.go | 5 +- coderd/workspacebuilds.go | 11 ++- coderd/workspaces.go | 44 ++++++++-- coderd/workspacestats/reporter.go | 12 ++- coderd/wspubsub/wspubsub.go | 71 ++++++++++++++++ codersdk/workspaces.go | 7 -- 21 files changed, 396 insertions(+), 259 deletions(-) create mode 100644 coderd/wspubsub/wspubsub.go diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index f69f366b43d4e..62fe6fad8d4de 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/tailnet" @@ -45,14 +46,15 @@ type API struct { *ScriptsAPI *tailnet.DRPCService - mu sync.Mutex - cachedWorkspaceID uuid.UUID + mu sync.Mutex } var _ agentproto.DRPCAgentServer = &API{} type Options struct { - AgentID uuid.UUID + AgentID uuid.UUID + OwnerID uuid.UUID + WorkspaceID uuid.UUID Ctx context.Context Log slog.Logger @@ -62,7 +64,7 @@ type Options struct { TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] StatsReporter *workspacestats.Reporter AppearanceFetcher *atomic.Pointer[appearance.Fetcher] - PublishWorkspaceUpdateFn func(ctx context.Context, workspaceID uuid.UUID) + PublishWorkspaceUpdateFn func(ctx context.Context, userID uuid.UUID, event wspubsub.WorkspaceEvent) PublishWorkspaceAgentLogsUpdateFn func(ctx context.Context, workspaceAgentID uuid.UUID, msg agentsdk.LogsNotifyMessage) NetworkTelemetryHandler func(batch []*tailnetproto.TelemetryEvent) @@ -75,18 +77,13 @@ type Options struct { ExternalAuthConfigs []*externalauth.Config Experiments codersdk.Experiments - // Optional: - // WorkspaceID avoids a future lookup to find the workspace ID by setting - // the cache in advance. - WorkspaceID uuid.UUID UpdateAgentMetricsFn func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) } func New(opts Options) *API { api := &API{ - opts: opts, - mu: sync.Mutex{}, - cachedWorkspaceID: opts.WorkspaceID, + opts: opts, + mu: sync.Mutex{}, } api.ManifestAPI = &ManifestAPI{ @@ -98,16 +95,7 @@ func New(opts Options) *API { AgentFn: api.agent, Database: opts.Database, DerpMapFn: opts.DerpMapFn, - WorkspaceIDFn: func(ctx context.Context, wa *database.WorkspaceAgent) (uuid.UUID, error) { - if opts.WorkspaceID != uuid.Nil { - return opts.WorkspaceID, nil - } - ws, err := opts.Database.GetWorkspaceByAgentID(ctx, wa.ID) - if err != nil { - return uuid.Nil, err - } - return ws.ID, nil - }, + WorkspaceID: opts.WorkspaceID, } api.AnnouncementBannerAPI = &AnnouncementBannerAPI{ @@ -125,7 +113,7 @@ func New(opts Options) *API { api.LifecycleAPI = &LifecycleAPI{ AgentFn: api.agent, - WorkspaceIDFn: api.workspaceID, + WorkspaceID: opts.WorkspaceID, Database: opts.Database, Log: opts.Log, PublishWorkspaceUpdateFn: api.publishWorkspaceUpdate, @@ -209,39 +197,11 @@ func (a *API) agent(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil } -func (a *API) workspaceID(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - a.mu.Lock() - if a.cachedWorkspaceID != uuid.Nil { - id := a.cachedWorkspaceID - a.mu.Unlock() - return id, nil - } - - if agent == nil { - agnt, err := a.agent(ctx) - if err != nil { - return uuid.Nil, err - } - agent = &agnt - } - - getWorkspaceAgentByIDRow, err := a.opts.Database.GetWorkspaceByAgentID(ctx, agent.ID) - if err != nil { - return uuid.Nil, xerrors.Errorf("get workspace by agent id %q: %w", agent.ID, err) - } - - a.mu.Lock() - a.cachedWorkspaceID = getWorkspaceAgentByIDRow.ID - a.mu.Unlock() - return getWorkspaceAgentByIDRow.ID, nil -} - -func (a *API) publishWorkspaceUpdate(ctx context.Context, agent *database.WorkspaceAgent) error { - workspaceID, err := a.workspaceID(ctx, agent) - if err != nil { - return err - } - - a.opts.PublishWorkspaceUpdateFn(ctx, workspaceID) +func (a *API) publishWorkspaceUpdate(ctx context.Context, agent *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { + a.opts.PublishWorkspaceUpdateFn(ctx, a.opts.OwnerID, wspubsub.WorkspaceEvent{ + Kind: kind, + WorkspaceID: a.opts.WorkspaceID, + AgentID: &agent.ID, + }) return nil } diff --git a/coderd/agentapi/apps.go b/coderd/agentapi/apps.go index b8aefa8883c3b..956e154e89d0d 100644 --- a/coderd/agentapi/apps.go +++ b/coderd/agentapi/apps.go @@ -9,13 +9,14 @@ import ( "cdr.dev/slog" agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/wspubsub" ) type AppsAPI struct { AgentFn func(context.Context) (database.WorkspaceAgent, error) Database database.Store Log slog.Logger - PublishWorkspaceUpdateFn func(context.Context, *database.WorkspaceAgent) error + PublishWorkspaceUpdateFn func(context.Context, *database.WorkspaceAgent, wspubsub.WorkspaceEventKind) error } func (a *AppsAPI) BatchUpdateAppHealths(ctx context.Context, req *agentproto.BatchUpdateAppHealthRequest) (*agentproto.BatchUpdateAppHealthResponse, error) { @@ -96,7 +97,7 @@ func (a *AppsAPI) BatchUpdateAppHealths(ctx context.Context, req *agentproto.Bat } if a.PublishWorkspaceUpdateFn != nil && len(newApps) > 0 { - err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent) + err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent, wspubsub.WorkspaceEventKindAppHealthUpdate) if err != nil { return nil, xerrors.Errorf("publish workspace update: %w", err) } diff --git a/coderd/agentapi/apps_test.go b/coderd/agentapi/apps_test.go index c774c6777b32a..e212a093ae002 100644 --- a/coderd/agentapi/apps_test.go +++ b/coderd/agentapi/apps_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/v2/coderd/agentapi" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/wspubsub" ) func TestBatchUpdateAppHealths(t *testing.T) { @@ -62,7 +63,7 @@ func TestBatchUpdateAppHealths(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishCalled = true return nil }, @@ -100,7 +101,7 @@ func TestBatchUpdateAppHealths(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishCalled = true return nil }, @@ -139,7 +140,7 @@ func TestBatchUpdateAppHealths(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishCalled = true return nil }, diff --git a/coderd/agentapi/lifecycle.go b/coderd/agentapi/lifecycle.go index e5211e804a7c4..5dd5e7b0c1b06 100644 --- a/coderd/agentapi/lifecycle.go +++ b/coderd/agentapi/lifecycle.go @@ -15,6 +15,7 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/wspubsub" ) type contextKeyAPIVersion struct{} @@ -25,10 +26,10 @@ func WithAPIVersion(ctx context.Context, version string) context.Context { type LifecycleAPI struct { AgentFn func(context.Context) (database.WorkspaceAgent, error) - WorkspaceIDFn func(context.Context, *database.WorkspaceAgent) (uuid.UUID, error) + WorkspaceID uuid.UUID Database database.Store Log slog.Logger - PublishWorkspaceUpdateFn func(context.Context, *database.WorkspaceAgent) error + PublishWorkspaceUpdateFn func(context.Context, *database.WorkspaceAgent, wspubsub.WorkspaceEventKind) error TimeNowFn func() time.Time // defaults to dbtime.Now() } @@ -45,13 +46,9 @@ func (a *LifecycleAPI) UpdateLifecycle(ctx context.Context, req *agentproto.Upda if err != nil { return nil, err } - workspaceID, err := a.WorkspaceIDFn(ctx, &workspaceAgent) - if err != nil { - return nil, err - } logger := a.Log.With( - slog.F("workspace_id", workspaceID), + slog.F("workspace_id", a.WorkspaceID), slog.F("payload", req), ) logger.Debug(ctx, "workspace agent state report") @@ -122,7 +119,7 @@ func (a *LifecycleAPI) UpdateLifecycle(ctx context.Context, req *agentproto.Upda } if a.PublishWorkspaceUpdateFn != nil { - err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent) + err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent, wspubsub.WorkspaceEventKindAgentLifecycleUpdate) if err != nil { return nil, xerrors.Errorf("publish workspace update: %w", err) } @@ -140,15 +137,11 @@ func (a *LifecycleAPI) UpdateStartup(ctx context.Context, req *agentproto.Update if err != nil { return nil, err } - workspaceID, err := a.WorkspaceIDFn(ctx, &workspaceAgent) - if err != nil { - return nil, err - } a.Log.Debug( ctx, "post workspace agent version", - slog.F("workspace_id", workspaceID), + slog.F("workspace_id", a.WorkspaceID), slog.F("agent_version", req.Startup.Version), ) diff --git a/coderd/agentapi/lifecycle_test.go b/coderd/agentapi/lifecycle_test.go index fe1469db0aa99..5ec6834d6b878 100644 --- a/coderd/agentapi/lifecycle_test.go +++ b/coderd/agentapi/lifecycle_test.go @@ -19,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/wspubsub" ) func TestUpdateLifecycle(t *testing.T) { @@ -69,12 +70,10 @@ func TestUpdateLifecycle(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agentCreated, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent) error { + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), + PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishCalled = true return nil }, @@ -111,11 +110,9 @@ func TestUpdateLifecycle(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agentStarting, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), // Test that nil publish fn works. PublishWorkspaceUpdateFn: nil, } @@ -156,12 +153,10 @@ func TestUpdateLifecycle(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agentCreated, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent) error { + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), + PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishCalled = true return nil }, @@ -204,9 +199,7 @@ func TestUpdateLifecycle(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agentCreated, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, + WorkspaceID: workspaceID, Database: dbM, Log: slogtest.Make(t, nil), PublishWorkspaceUpdateFn: nil, @@ -239,12 +232,10 @@ func TestUpdateLifecycle(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent) error { + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), + PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { atomic.AddInt64(&publishCalled, 1) return nil }, @@ -314,12 +305,10 @@ func TestUpdateLifecycle(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agentCreated, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent) error { + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), + PublishWorkspaceUpdateFn: func(ctx context.Context, agent *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishCalled = true return nil }, @@ -354,11 +343,9 @@ func TestUpdateStartup(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), // Not used by UpdateStartup. PublishWorkspaceUpdateFn: nil, } @@ -402,11 +389,9 @@ func TestUpdateStartup(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), // Not used by UpdateStartup. PublishWorkspaceUpdateFn: nil, } @@ -435,11 +420,9 @@ func TestUpdateStartup(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil }, - WorkspaceIDFn: func(ctx context.Context, agent *database.WorkspaceAgent) (uuid.UUID, error) { - return workspaceID, nil - }, - Database: dbM, - Log: slogtest.Make(t, nil), + WorkspaceID: workspaceID, + Database: dbM, + Log: slogtest.Make(t, nil), // Not used by UpdateStartup. PublishWorkspaceUpdateFn: nil, } diff --git a/coderd/agentapi/logs.go b/coderd/agentapi/logs.go index 809137525fd04..1d63f32b7b0dd 100644 --- a/coderd/agentapi/logs.go +++ b/coderd/agentapi/logs.go @@ -11,6 +11,7 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk/agentsdk" ) @@ -18,7 +19,7 @@ type LogsAPI struct { AgentFn func(context.Context) (database.WorkspaceAgent, error) Database database.Store Log slog.Logger - PublishWorkspaceUpdateFn func(context.Context, *database.WorkspaceAgent) error + PublishWorkspaceUpdateFn func(context.Context, *database.WorkspaceAgent, wspubsub.WorkspaceEventKind) error PublishWorkspaceAgentLogsUpdateFn func(ctx context.Context, workspaceAgentID uuid.UUID, msg agentsdk.LogsNotifyMessage) TimeNowFn func() time.Time // defaults to dbtime.Now() @@ -123,7 +124,7 @@ func (a *LogsAPI) BatchCreateLogs(ctx context.Context, req *agentproto.BatchCrea } if a.PublishWorkspaceUpdateFn != nil { - err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent) + err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent, wspubsub.WorkspaceEventKindAgentLogsOverflow) if err != nil { return nil, xerrors.Errorf("publish workspace update: %w", err) } @@ -143,7 +144,7 @@ func (a *LogsAPI) BatchCreateLogs(ctx context.Context, req *agentproto.BatchCrea if workspaceAgent.LogsLength == 0 && a.PublishWorkspaceUpdateFn != nil { // If these are the first logs being appended, we publish a UI update // to notify the UI that logs are now available. - err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent) + err = a.PublishWorkspaceUpdateFn(ctx, &workspaceAgent, wspubsub.WorkspaceEventKindAgentFirstLogs) if err != nil { return nil, xerrors.Errorf("publish workspace update: %w", err) } diff --git a/coderd/agentapi/logs_test.go b/coderd/agentapi/logs_test.go index 261b6c8f6ea83..8e6638ba82624 100644 --- a/coderd/agentapi/logs_test.go +++ b/coderd/agentapi/logs_test.go @@ -19,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk/agentsdk" ) @@ -50,7 +51,7 @@ func TestBatchCreateLogs(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishWorkspaceUpdateCalled = true return nil }, @@ -154,7 +155,7 @@ func TestBatchCreateLogs(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishWorkspaceUpdateCalled = true return nil }, @@ -202,7 +203,7 @@ func TestBatchCreateLogs(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishWorkspaceUpdateCalled = true return nil }, @@ -295,7 +296,7 @@ func TestBatchCreateLogs(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishWorkspaceUpdateCalled = true return nil }, @@ -339,7 +340,7 @@ func TestBatchCreateLogs(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishWorkspaceUpdateCalled = true return nil }, @@ -386,7 +387,7 @@ func TestBatchCreateLogs(t *testing.T) { }, Database: dbM, Log: slogtest.Make(t, nil), - PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent) error { + PublishWorkspaceUpdateFn: func(ctx context.Context, wa *database.WorkspaceAgent, kind wspubsub.WorkspaceEventKind) error { publishWorkspaceUpdateCalled = true return nil }, diff --git a/coderd/agentapi/manifest.go b/coderd/agentapi/manifest.go index a58bf6941cb04..fd4d38d4a75ab 100644 --- a/coderd/agentapi/manifest.go +++ b/coderd/agentapi/manifest.go @@ -29,11 +29,11 @@ type ManifestAPI struct { ExternalAuthConfigs []*externalauth.Config DisableDirectConnections bool DerpForceWebSockets bool + WorkspaceID uuid.UUID - AgentFn func(context.Context) (database.WorkspaceAgent, error) - WorkspaceIDFn func(context.Context, *database.WorkspaceAgent) (uuid.UUID, error) - Database database.Store - DerpMapFn func() *tailcfg.DERPMap + AgentFn func(context.Context) (database.WorkspaceAgent, error) + Database database.Store + DerpMapFn func() *tailcfg.DERPMap } func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifestRequest) (*agentproto.Manifest, error) { @@ -41,11 +41,6 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest if err != nil { return nil, err } - workspaceID, err := a.WorkspaceIDFn(ctx, &workspaceAgent) - if err != nil { - return nil, err - } - var ( dbApps []database.WorkspaceApp scripts []database.WorkspaceAgentScript @@ -75,7 +70,7 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest return err }) eg.Go(func() (err error) { - workspace, err = a.Database.GetWorkspaceByID(ctx, workspaceID) + workspace, err = a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) if err != nil { return xerrors.Errorf("getting workspace by id: %w", err) } diff --git a/coderd/agentapi/manifest_test.go b/coderd/agentapi/manifest_test.go index e7a36081f64b4..2cde35ba03ab9 100644 --- a/coderd/agentapi/manifest_test.go +++ b/coderd/agentapi/manifest_test.go @@ -288,11 +288,9 @@ func TestGetManifest(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil }, - WorkspaceIDFn: func(ctx context.Context, _ *database.WorkspaceAgent) (uuid.UUID, error) { - return workspace.ID, nil - }, - Database: mDB, - DerpMapFn: derpMapFn, + WorkspaceID: workspace.ID, + Database: mDB, + DerpMapFn: derpMapFn, } mDB.EXPECT().GetWorkspaceAppsByAgentID(gomock.Any(), agent.ID).Return(apps, nil) @@ -355,11 +353,9 @@ func TestGetManifest(t *testing.T) { AgentFn: func(ctx context.Context) (database.WorkspaceAgent, error) { return agent, nil }, - WorkspaceIDFn: func(ctx context.Context, _ *database.WorkspaceAgent) (uuid.UUID, error) { - return workspace.ID, nil - }, - Database: mDB, - DerpMapFn: derpMapFn, + WorkspaceID: workspace.ID, + Database: mDB, + DerpMapFn: derpMapFn, } mDB.EXPECT().GetWorkspaceAppsByAgentID(gomock.Any(), agent.ID).Return(apps, nil) diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index 83edb8cccc4e1..3ebf99aa6bc4b 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/coderd/workspacestats/workspacestatstest" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -153,12 +154,19 @@ func TestUpdateStates(t *testing.T) { }).Return(nil) // Ensure that pubsub notifications are sent. - notifyDescription := make(chan []byte) - ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, description []byte) { - go func() { - notifyDescription <- description - }() - }) + notifyDescription := make(chan struct{}) + ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStatsUpdate && e.WorkspaceID == workspace.ID { + go func() { + notifyDescription <- struct{}{} + }() + } + })) resp, err := api.UpdateStats(context.Background(), req) require.NoError(t, err) @@ -183,8 +191,7 @@ func TestUpdateStates(t *testing.T) { select { case <-ctx.Done(): t.Error("timed out while waiting for pubsub notification") - case description := <-notifyDescription: - require.Equal(t, description, []byte{}) + case <-notifyDescription: } require.True(t, updateAgentMetricsFnCalled) }) @@ -495,12 +502,19 @@ func TestUpdateStates(t *testing.T) { dbM.EXPECT().GetUserByID(gomock.Any(), user.ID).Return(user, nil) // Ensure that pubsub notifications are sent. - notifyDescription := make(chan []byte) - ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, description []byte) { - go func() { - notifyDescription <- description - }() - }) + notifyDescription := make(chan struct{}) + ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStatsUpdate && e.WorkspaceID == workspace.ID { + go func() { + notifyDescription <- struct{}{} + }() + } + })) resp, err := api.UpdateStats(context.Background(), req) require.NoError(t, err) @@ -523,8 +537,7 @@ func TestUpdateStates(t *testing.T) { select { case <-ctx.Done(): t.Error("timed out while waiting for pubsub notification") - case description := <-notifyDescription: - require.Equal(t, description, []byte{}) + case <-notifyDescription: } require.True(t, updateAgentMetricsFnCalled) }) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 616dd2afac619..3ff9f59fa138e 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -19,7 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/telemetry" - "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" ) @@ -225,7 +225,12 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse { _ = dbgen.WorkspaceBuildParameters(b.t, b.db, b.params) if b.ps != nil { - err = b.ps.Publish(codersdk.WorkspaceNotifyChannel(resp.Build.WorkspaceID), []byte{}) + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: resp.Workspace.ID, + }) + require.NoError(b.t, err) + err = b.ps.Publish(wspubsub.WorkspaceEventChannel(resp.Workspace.OwnerID), msg) require.NoError(b.t, err) } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 6c72ff5831947..9a9da3d8112ab 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -39,6 +39,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/tracing" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/provisioner" @@ -493,7 +494,15 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo for _, group := range ownerGroups { ownerGroupNames = append(ownerGroupNames, group.Group.Name) } - err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspace.ID), []byte{}) + + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: workspace.ID, + }) + if err != nil { + return nil, failJob(fmt.Sprintf("marshal workspace update event: %s", err)) + } + err = s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg) if err != nil { return nil, failJob(fmt.Sprintf("publish workspace update: %s", err)) } @@ -1023,9 +1032,16 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto. s.notifyWorkspaceBuildFailed(ctx, workspace, build) - err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), []byte{}) + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: workspace.ID, + }) if err != nil { - return nil, xerrors.Errorf("update workspace: %w", err) + return nil, xerrors.Errorf("marshal workspace update event: %s", err) + } + err = s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg) + if err != nil { + return nil, xerrors.Errorf("publish workspace update: %w", err) } case *proto.FailedJob_TemplateImport_: } @@ -1369,9 +1385,6 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) return nil, xerrors.Errorf("update provisioner job: %w", err) } s.Logger.Debug(ctx, "marked import job as completed", slog.F("job_id", jobID)) - if err != nil { - return nil, xerrors.Errorf("complete job: %w", err) - } case *proto.CompletedJob_WorkspaceBuild_: var input WorkspaceProvisionJob err = json.Unmarshal(job.Input, &input) @@ -1491,7 +1504,15 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) return case <-wait: // Wait for the next potential timeout to occur. - if err := s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceBuild.WorkspaceID), []byte{}); err != nil { + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentTimeout, + WorkspaceID: workspace.ID, + }) + if err != nil { + s.Logger.Error(ctx, "marshal workspace update event", slog.Error(err)) + break + } + if err := s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg); err != nil { if s.lifecycleCtx.Err() != nil { // If the server is shutting down, we don't want to log this error, nor wait around. s.Logger.Debug(ctx, "stopping notifications due to server shutdown", @@ -1608,7 +1629,14 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) }) } - err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceBuild.WorkspaceID), []byte{}) + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: workspace.ID, + }) + if err != nil { + return nil, xerrors.Errorf("marshal workspace update event: %s", err) + } + err = s.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg) if err != nil { return nil, xerrors.Errorf("update workspace: %w", err) } @@ -1639,9 +1667,6 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) return nil, xerrors.Errorf("update provisioner job: %w", err) } s.Logger.Debug(ctx, "marked template dry-run job as completed", slog.F("job_id", jobID)) - if err != nil { - return nil, xerrors.Errorf("complete job: %w", err) - } default: if completed.Type == nil { diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index baa53b92d74e2..98ab07db3d0f7 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -40,6 +40,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/schedule/cron" "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -295,12 +296,19 @@ func TestAcquireJob(t *testing.T) { startPublished := make(chan struct{}) var closed bool - closeStartSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) { - if !closed { - close(startPublished) - closed = true - } - }) + closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + if !closed { + close(startPublished) + closed = true + } + } + })) require.NoError(t, err) defer closeStartSubscribe() @@ -398,9 +406,16 @@ func TestAcquireJob(t *testing.T) { }) stopPublished := make(chan struct{}) - closeStopSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) { - close(stopPublished) - }) + closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + close(stopPublished) + } + })) require.NoError(t, err) defer closeStopSubscribe() @@ -874,12 +889,11 @@ func TestFailJob(t *testing.T) { auditor: auditor, }) org := dbgen.Organization(t, db, database.Organization{}) - workspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{ + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ ID: uuid.New(), AutomaticUpdates: database.AutomaticUpdatesNever, OrganizationID: org.ID, }) - require.NoError(t, err) buildID := uuid.New() input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: buildID, @@ -889,6 +903,7 @@ func TestFailJob(t *testing.T) { job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ ID: uuid.New(), Input: input, + InitiatorID: workspace.OwnerID, Provisioner: database.ProvisionerTypeEcho, Type: database.ProvisionerJobTypeWorkspaceBuild, StorageMethod: database.ProvisionerStorageMethodFile, @@ -897,6 +912,7 @@ func TestFailJob(t *testing.T) { err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ ID: buildID, WorkspaceID: workspace.ID, + InitiatorID: workspace.OwnerID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, JobID: job.ID, @@ -913,9 +929,16 @@ func TestFailJob(t *testing.T) { require.NoError(t, err) publishedWorkspace := make(chan struct{}) - closeWorkspaceSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) { - close(publishedWorkspace) - }) + closeWorkspaceSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + close(publishedWorkspace) + } + })) require.NoError(t, err) defer closeWorkspaceSubscribe() publishedLogs := make(chan struct{}) @@ -1279,13 +1302,15 @@ func TestCompleteJob(t *testing.T) { }) build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: workspaceTable.ID, + InitiatorID: user.ID, TemplateVersionID: version.ID, Transition: c.transition, Reason: database.BuildReasonInitiator, }) job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, + FileID: file.ID, + InitiatorID: user.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: build.ID, })), @@ -1302,9 +1327,16 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) publishedWorkspace := make(chan struct{}) - closeWorkspaceSubscribe, err := ps.Subscribe(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), func(_ context.Context, _ []byte) { - close(publishedWorkspace) - }) + closeWorkspaceSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspaceTable.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspaceTable.ID { + close(publishedWorkspace) + } + })) require.NoError(t, err) defer closeWorkspaceSubscribe() publishedLogs := make(chan struct{}) @@ -1643,8 +1675,9 @@ func TestNotifications(t *testing.T) { Reason: tc.deletionReason, }) job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, + FileID: file.ID, + InitiatorID: initiator.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: build.ID, })), @@ -1761,8 +1794,9 @@ func TestNotifications(t *testing.T) { Reason: tc.buildReason, }) job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, + FileID: file.ID, + InitiatorID: initiator.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: build.ID, })), @@ -1833,6 +1867,7 @@ func TestNotifications(t *testing.T) { }) job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ FileID: dbgen.File(t, db, database.File{CreatedBy: user.ID}).ID, + InitiatorID: user.ID, Type: database.ProvisionerJobTypeWorkspaceBuild, Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{WorkspaceBuildID: build.ID})), OrganizationID: pd.OrganizationID, diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a181697f27279..14e986123edb7 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -34,6 +34,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -242,25 +243,20 @@ func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request) api.Logger.Warn(ctx, "failed to update workspace agent log overflow", slog.Error(err)) } - resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to get workspace resource.", + Message: "Failed to get workspace.", Detail: err.Error(), }) return } - build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Internal error fetching workspace build job.", - Detail: err.Error(), - }) - return - } - - api.publishWorkspaceUpdate(ctx, build.WorkspaceID) + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentLogsOverflow, + WorkspaceID: workspace.ID, + AgentID: &workspaceAgent.ID, + }) httpapi.Write(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.Response{ Message: "Logs limit exceeded", @@ -279,25 +275,20 @@ func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request) if workspaceAgent.LogsLength == 0 { // If these are the first logs being appended, we publish a UI update // to notify the UI that logs are now available. - resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to get workspace resource.", + Message: "Failed to get workspace.", Detail: err.Error(), }) return } - build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Internal error fetching workspace build job.", - Detail: err.Error(), - }) - return - } - - api.publishWorkspaceUpdate(ctx, build.WorkspaceID) + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentFirstLogs, + WorkspaceID: workspace.ID, + AgentID: &workspaceAgent.ID, + }) } httpapi.Write(ctx, rw, http.StatusOK, nil) @@ -426,12 +417,19 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { notifyCh <- struct{}{} // Subscribe to workspace to detect new builds. - closeSubscribeWorkspace, err := api.Pubsub.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), func(_ context.Context, _ []byte) { - select { - case workspaceNotifyCh <- struct{}{}: - default: - } - }) + closeSubscribeWorkspace, err := api.Pubsub.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + select { + case workspaceNotifyCh <- struct{}{}: + default: + } + } + })) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Failed to subscribe to workspace for log streaming.", diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index a47fa0c12ed1a..29f2ad476dca0 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -26,6 +26,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" @@ -132,11 +133,13 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { closeCtx, closeCtxCancel := context.WithCancel(ctx) defer closeCtxCancel() - monitor := api.startAgentYamuxMonitor(closeCtx, workspaceAgent, build, mux) + monitor := api.startAgentYamuxMonitor(closeCtx, workspace, workspaceAgent, build, mux) defer monitor.close() agentAPI := agentapi.New(agentapi.Options{ - AgentID: workspaceAgent.ID, + AgentID: workspaceAgent.ID, + OwnerID: workspace.OwnerID, + WorkspaceID: workspace.ID, Ctx: api.ctx, Log: logger, @@ -160,7 +163,6 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { Experiments: api.Experiments, // Optional: - WorkspaceID: build.WorkspaceID, // saves the extra lookup later UpdateAgentMetricsFn: api.UpdateAgentMetrics, }) @@ -225,11 +227,14 @@ func (y *yamuxPingerCloser) Ping(ctx context.Context) error { } func (api *API) startAgentYamuxMonitor(ctx context.Context, - workspaceAgent database.WorkspaceAgent, workspaceBuild database.WorkspaceBuild, + workspace database.Workspace, + workspaceAgent database.WorkspaceAgent, + workspaceBuild database.WorkspaceBuild, mux *yamux.Session, ) *agentConnectionMonitor { monitor := &agentConnectionMonitor{ apiCtx: api.ctx, + workspace: workspace, workspaceAgent: workspaceAgent, workspaceBuild: workspaceBuild, conn: &yamuxPingerCloser{mux: mux}, @@ -250,7 +255,7 @@ func (api *API) startAgentYamuxMonitor(ctx context.Context, } type workspaceUpdater interface { - publishWorkspaceUpdate(ctx context.Context, workspaceID uuid.UUID) + publishWorkspaceUpdate(ctx context.Context, ownerID uuid.UUID, event wspubsub.WorkspaceEvent) } type pingerCloser interface { @@ -262,6 +267,7 @@ type agentConnectionMonitor struct { apiCtx context.Context cancel context.CancelFunc wg sync.WaitGroup + workspace database.Workspace workspaceAgent database.WorkspaceAgent workspaceBuild database.WorkspaceBuild conn pingerCloser @@ -393,7 +399,11 @@ func (m *agentConnectionMonitor) monitor(ctx context.Context) { ) } } - m.updater.publishWorkspaceUpdate(finalCtx, m.workspaceBuild.WorkspaceID) + m.updater.publishWorkspaceUpdate(finalCtx, m.workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentConnectionUpdate, + WorkspaceID: m.workspaceBuild.WorkspaceID, + AgentID: &m.workspaceAgent.ID, + }) }() reason := "disconnect" defer func() { @@ -407,7 +417,11 @@ func (m *agentConnectionMonitor) monitor(ctx context.Context) { reason = err.Error() return } - m.updater.publishWorkspaceUpdate(ctx, m.workspaceBuild.WorkspaceID) + m.updater.publishWorkspaceUpdate(ctx, m.workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentConnectionUpdate, + WorkspaceID: m.workspaceBuild.WorkspaceID, + AgentID: &m.workspaceAgent.ID, + }) ticker := time.NewTicker(m.pingPeriod) defer ticker.Stop() @@ -441,7 +455,11 @@ func (m *agentConnectionMonitor) monitor(ctx context.Context) { return } if connectionStatusChanged { - m.updater.publishWorkspaceUpdate(ctx, m.workspaceBuild.WorkspaceID) + m.updater.publishWorkspaceUpdate(ctx, m.workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentConnectionUpdate, + WorkspaceID: m.workspaceBuild.WorkspaceID, + AgentID: &m.workspaceAgent.ID, + }) } err = checkBuildIsLatest(ctx, m.db, m.workspaceBuild) if err != nil { diff --git a/coderd/workspaceagentsrpc_internal_test.go b/coderd/workspaceagentsrpc_internal_test.go index dbae11a218619..338c2e4899368 100644 --- a/coderd/workspaceagentsrpc_internal_test.go +++ b/coderd/workspaceagentsrpc_internal_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -356,10 +357,10 @@ type fakeUpdater struct { updates []uuid.UUID } -func (f *fakeUpdater) publishWorkspaceUpdate(_ context.Context, workspaceID uuid.UUID) { +func (f *fakeUpdater) publishWorkspaceUpdate(_ context.Context, _ uuid.UUID, event wspubsub.WorkspaceEvent) { f.Lock() defer f.Unlock() - f.updates = append(f.updates, workspaceID) + f.updates = append(f.updates, event.WorkspaceID) } func (f *fakeUpdater) requireEventuallySomeUpdates(t *testing.T, workspaceID uuid.UUID) { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 3515bc4a944b5..da785ac3a5a8a 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -30,6 +30,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/wsbuilder" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" ) @@ -412,7 +413,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - api.publishWorkspaceUpdate(ctx, workspace.ID) + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: workspace.ID, + }) httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) } @@ -491,7 +495,10 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques return } - api.publishWorkspaceUpdate(ctx, workspace.ID) + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: workspace.ID, + }) httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{ Message: "Job has been marked as canceled...", diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 394a728472b0d..4638596e66eae 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -34,6 +34,7 @@ import ( "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/wsbuilder" + "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" ) @@ -806,7 +807,11 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - api.publishWorkspaceUpdate(ctx, workspace.ID) + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindMetadataUpdate, + WorkspaceID: workspace.ID, + }) + aReq.New = newWorkspace rw.WriteHeader(http.StatusNoContent) @@ -1216,7 +1221,11 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { if err != nil { api.Logger.Info(ctx, "extending workspace", slog.Error(err)) } - api.publishWorkspaceUpdate(ctx, workspace.ID) + + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindMetadataUpdate, + WorkspaceID: workspace.ID, + }) httpapi.Write(ctx, rw, code, resp) } @@ -1667,7 +1676,17 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) } - cancelWorkspaceSubscribe, err := api.Pubsub.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), sendUpdate) + cancelWorkspaceSubscribe, err := api.Pubsub.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(ctx context.Context, payload wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if payload.WorkspaceID != workspace.ID { + return + } + sendUpdate(ctx, nil) + })) if err != nil { _ = sendEvent(ctx, codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeError, @@ -2006,11 +2025,24 @@ func validWorkspaceSchedule(s *string) (sql.NullString, error) { }, nil } -func (api *API) publishWorkspaceUpdate(ctx context.Context, workspaceID uuid.UUID) { - err := api.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceID), []byte{}) +func (api *API) publishWorkspaceUpdate(ctx context.Context, ownerID uuid.UUID, event wspubsub.WorkspaceEvent) { + err := event.Validate() + if err != nil { + api.Logger.Warn(ctx, "invalid workspace update event", + slog.F("workspace_id", event.WorkspaceID), + slog.F("event_kind", event.Kind), slog.Error(err)) + return + } + msg, err := json.Marshal(event) + if err != nil { + api.Logger.Warn(ctx, "failed to marshal workspace update", + slog.F("workspace_id", event.WorkspaceID), slog.Error(err)) + return + } + err = api.Pubsub.Publish(wspubsub.WorkspaceEventChannel(ownerID), msg) if err != nil { api.Logger.Warn(ctx, "failed to publish workspace update", - slog.F("workspace_id", workspaceID), slog.Error(err)) + slog.F("workspace_id", event.WorkspaceID), slog.Error(err)) } } diff --git a/coderd/workspacestats/reporter.go b/coderd/workspacestats/reporter.go index e59a9f15d5e95..b00523b1ad5d4 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -2,6 +2,7 @@ package workspacestats import ( "context" + "encoding/json" "sync/atomic" "time" @@ -18,7 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/coderd/workspaceapps" - "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/coderd/wspubsub" ) type ReporterOptions struct { @@ -174,7 +175,14 @@ func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspac r.opts.UsageTracker.Add(workspace.ID) // notify workspace update - err := r.opts.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspace.ID), []byte{}) + msg, err := json.Marshal(wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStatsUpdate, + WorkspaceID: workspace.ID, + }) + if err != nil { + return xerrors.Errorf("marshal workspace agent stats event: %w", err) + } + err = r.opts.Pubsub.Publish(wspubsub.WorkspaceEventChannel(workspace.OwnerID), msg) if err != nil { r.opts.Logger.Warn(ctx, "failed to publish workspace agent stats", slog.F("workspace_id", workspace.ID), slog.Error(err)) diff --git a/coderd/wspubsub/wspubsub.go b/coderd/wspubsub/wspubsub.go new file mode 100644 index 0000000000000..0326efa695304 --- /dev/null +++ b/coderd/wspubsub/wspubsub.go @@ -0,0 +1,71 @@ +package wspubsub + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "golang.org/x/xerrors" +) + +// WorkspaceEventChannel can be used to subscribe to events for +// workspaces owned by the provided user ID. +func WorkspaceEventChannel(ownerID uuid.UUID) string { + return fmt.Sprintf("workspace_owner:%s", ownerID) +} + +func HandleWorkspaceEvent(cb func(ctx context.Context, payload WorkspaceEvent, err error)) func(ctx context.Context, message []byte, err error) { + return func(ctx context.Context, message []byte, err error) { + if err != nil { + cb(ctx, WorkspaceEvent{}, xerrors.Errorf("workspace event pubsub: %w", err)) + return + } + var payload WorkspaceEvent + if err := json.Unmarshal(message, &payload); err != nil { + cb(ctx, WorkspaceEvent{}, xerrors.Errorf("unmarshal workspace event")) + return + } + if err := payload.Validate(); err != nil { + cb(ctx, payload, xerrors.Errorf("validate workspace event")) + return + } + cb(ctx, payload, err) + } +} + +type WorkspaceEvent struct { + Kind WorkspaceEventKind `json:"kind"` + WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` + // AgentID is only set for WorkspaceEventKindAgent* events + // (excluding AgentTimeout) + AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"` +} + +type WorkspaceEventKind string + +const ( + WorkspaceEventKindStateChange WorkspaceEventKind = "state_change" + WorkspaceEventKindStatsUpdate WorkspaceEventKind = "stats_update" + WorkspaceEventKindMetadataUpdate WorkspaceEventKind = "mtd_update" + WorkspaceEventKindAppHealthUpdate WorkspaceEventKind = "app_health" + + WorkspaceEventKindAgentLifecycleUpdate WorkspaceEventKind = "agt_lifecycle_update" + WorkspaceEventKindAgentConnectionUpdate WorkspaceEventKind = "agt_connection_update" + WorkspaceEventKindAgentFirstLogs WorkspaceEventKind = "agt_first_logs" + WorkspaceEventKindAgentLogsOverflow WorkspaceEventKind = "agt_logs_overflow" + WorkspaceEventKindAgentTimeout WorkspaceEventKind = "agt_timeout" +) + +func (w *WorkspaceEvent) Validate() error { + if w.WorkspaceID == uuid.Nil { + return xerrors.New("workspaceID must be set") + } + if w.Kind == "" { + return xerrors.New("kind must be set") + } + if w.Kind == WorkspaceEventKindAgentLifecycleUpdate && w.AgentID == nil { + return xerrors.New("agentID must be set for Agent events") + } + return nil +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 5ce1769150e02..d6f3e30a92979 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -639,10 +639,3 @@ func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceB var timings WorkspaceBuildTimings return timings, json.NewDecoder(res.Body).Decode(&timings) } - -// WorkspaceNotifyChannel is the PostgreSQL NOTIFY -// channel to listen for updates on. The payload is empty, -// because the size of a workspace payload can be very large. -func WorkspaceNotifyChannel(id uuid.UUID) string { - return fmt.Sprintf("workspace:%s", id) -} From f941e780791829ce1bf1144431d2a7e402d9d8f9 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:36:12 +1100 Subject: [PATCH 019/223] chore: add db query to retrieve workspaces & their agents (#14792) Second PR for #14716. Adds a query that, given a user ID, returns all the workspaces they own, that can also be `ActionRead` by the requesting user. ``` type GetWorkspacesAndAgentsByOwnerIDRow struct { WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` WorkspaceName string `db:"workspace_name" json:"workspace_name"` JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"` Transition WorkspaceTransition `db:"transition" json:"transition"` Agents []AgentIDNamePair `db:"agents" json:"agents"` } ``` `JobStatus` and `Transition` are set using the latest build/job of the workspace. Deleted workspaces are not included. --- coderd/database/dbauthz/dbauthz.go | 12 + coderd/database/dbauthz/dbauthz_test.go | 18 ++ coderd/database/dbmem/dbmem.go | 66 +++++ coderd/database/dbmetrics/querymetrics.go | 14 + coderd/database/dbmock/dbmock.go | 30 +++ coderd/database/dump.sql | 5 + .../000273_workspace_updates.down.sql | 1 + .../000273_workspace_updates.up.sql | 4 + coderd/database/modelqueries.go | 44 ++++ coderd/database/querier.go | 1 + coderd/database/querier_test.go | 244 +++++++++++++++--- coderd/database/queries.sql.go | 75 ++++++ coderd/database/queries/workspaces.sql | 39 +++ coderd/database/sqlc.yaml | 3 + coderd/database/types.go | 33 +++ 15 files changed, 551 insertions(+), 38 deletions(-) create mode 100644 coderd/database/migrations/000273_workspace_updates.down.sql create mode 100644 coderd/database/migrations/000273_workspace_updates.up.sql diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 76d78754255ca..630e5e6165c6c 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2789,6 +2789,14 @@ func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesP return q.db.GetAuthorizedWorkspaces(ctx, arg, prep) } +func (q *querier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceWorkspace.Type) + if err != nil { + return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) + } + return q.db.GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, ownerID, prep) +} + func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.WorkspaceTable, error) { return q.db.GetWorkspacesEligibleForTransition(ctx, now) } @@ -4242,6 +4250,10 @@ func (q *querier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetW return q.GetWorkspaces(ctx, arg) } +func (q *querier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, _ rbac.PreparedAuthorized) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + return q.GetWorkspacesAndAgentsByOwnerID(ctx, ownerID) +} + // GetAuthorizedUsers is not required for dbauthz since GetUsers is already // authenticated. func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, _ rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 73403a95b7859..515330f2edefb 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1470,6 +1470,24 @@ func (s *MethodTestSuite) TestWorkspace() { // No asserts here because SQLFilter. check.Args(database.GetWorkspacesParams{}, emptyPreparedAuthorized{}).Asserts() })) + s.Run("GetWorkspacesAndAgentsByOwnerID", s.Subtest(func(db database.Store, check *expects) { + ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + _ = dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) + res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) + _ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) + // No asserts here because SQLFilter. + check.Args(ws.OwnerID).Asserts() + })) + s.Run("GetAuthorizedWorkspacesAndAgentsByOwnerID", s.Subtest(func(db database.Store, check *expects) { + ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + _ = dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) + res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) + _ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) + // No asserts here because SQLFilter. + check.Args(ws.OwnerID, emptyPreparedAuthorized{}).Asserts() + })) s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index e949b5be4880d..8214a9f6b77ff 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6839,6 +6839,11 @@ func (q *FakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspa return workspaceRows, err } +func (q *FakeQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + // No auth filter. + return q.GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, ownerID, nil) +} + func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.WorkspaceTable, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -11224,6 +11229,67 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount), arg.WithSummary), nil } +func (q *FakeQuerier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + if prepared != nil { + // Call this to match the same function calls as the SQL implementation. + _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL()) + if err != nil { + return nil, err + } + } + workspaces := make([]database.WorkspaceTable, 0) + for _, workspace := range q.workspaces { + if workspace.OwnerID == ownerID && !workspace.Deleted { + workspaces = append(workspaces, workspace) + } + } + + out := make([]database.GetWorkspacesAndAgentsByOwnerIDRow, 0, len(workspaces)) + for _, w := range workspaces { + // these always exist + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, w.ID) + if err != nil { + return nil, xerrors.Errorf("get latest build: %w", err) + } + + job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) + if err != nil { + return nil, xerrors.Errorf("get provisioner job: %w", err) + } + + outAgents := make([]database.AgentIDNamePair, 0) + resources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, job.ID) + if err != nil { + return nil, xerrors.Errorf("get workspace resources: %w", err) + } + if len(resources) > 0 { + agents, err := q.getWorkspaceAgentsByResourceIDsNoLock(ctx, []uuid.UUID{resources[0].ID}) + if err != nil { + return nil, xerrors.Errorf("get workspace agents: %w", err) + } + for _, a := range agents { + outAgents = append(outAgents, database.AgentIDNamePair{ + ID: a.ID, + Name: a.Name, + }) + } + } + + out = append(out, database.GetWorkspacesAndAgentsByOwnerIDRow{ + ID: w.ID, + Name: w.Name, + JobStatus: job.JobStatus, + Transition: build.Transition, + Agents: outAgents, + }) + } + + return out, nil +} + func (q *FakeQuerier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { if err := validateDatabaseType(arg); err != nil { return nil, err diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 7e74aab3b9de0..2d542be1160fd 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1645,6 +1645,13 @@ func (m queryMetricsStore) GetWorkspaces(ctx context.Context, arg database.GetWo return workspaces, err } +func (m queryMetricsStore) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspacesAndAgentsByOwnerID(ctx, ownerID) + m.queryLatencies.WithLabelValues("GetWorkspacesAndAgentsByOwnerID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.WorkspaceTable, error) { start := time.Now() workspaces, err := m.s.GetWorkspacesEligibleForTransition(ctx, now) @@ -2695,6 +2702,13 @@ func (m queryMetricsStore) GetAuthorizedWorkspaces(ctx context.Context, arg data return workspaces, err } +func (m queryMetricsStore) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + start := time.Now() + r0, r1 := m.s.GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, ownerID, prepared) + m.queryLatencies.WithLabelValues("GetAuthorizedWorkspacesAndAgentsByOwnerID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { start := time.Now() r0, r1 := m.s.GetAuthorizedUsers(ctx, arg, prepared) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index ffc9ab79f777e..39e82f2e82df5 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1057,6 +1057,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspaces(arg0, arg1, arg2 any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspaces", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspaces), arg0, arg1, arg2) } +// GetAuthorizedWorkspacesAndAgentsByOwnerID mocks base method. +func (m *MockStore) GetAuthorizedWorkspacesAndAgentsByOwnerID(arg0 context.Context, arg1 uuid.UUID, arg2 rbac.PreparedAuthorized) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthorizedWorkspacesAndAgentsByOwnerID", arg0, arg1, arg2) + ret0, _ := ret[0].([]database.GetWorkspacesAndAgentsByOwnerIDRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthorizedWorkspacesAndAgentsByOwnerID indicates an expected call of GetAuthorizedWorkspacesAndAgentsByOwnerID. +func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), arg0, arg1, arg2) +} + // GetCoordinatorResumeTokenSigningKey mocks base method. func (m *MockStore) GetCoordinatorResumeTokenSigningKey(arg0 context.Context) (string, error) { m.ctrl.T.Helper() @@ -3472,6 +3487,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaces(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaces", reflect.TypeOf((*MockStore)(nil).GetWorkspaces), arg0, arg1) } +// GetWorkspacesAndAgentsByOwnerID mocks base method. +func (m *MockStore) GetWorkspacesAndAgentsByOwnerID(arg0 context.Context, arg1 uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspacesAndAgentsByOwnerID", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspacesAndAgentsByOwnerIDRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspacesAndAgentsByOwnerID indicates an expected call of GetWorkspacesAndAgentsByOwnerID. +func (mr *MockStoreMockRecorder) GetWorkspacesAndAgentsByOwnerID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetWorkspacesAndAgentsByOwnerID), arg0, arg1) +} + // GetWorkspacesEligibleForTransition mocks base method. func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg1 time.Time) ([]database.WorkspaceTable, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index e4e119423ea78..557b5c2dd9325 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1,5 +1,10 @@ -- Code generated by 'make coderd/database/generate'. DO NOT EDIT. +CREATE TYPE agent_id_name_pair AS ( + id uuid, + name text +); + CREATE TYPE api_key_scope AS ENUM ( 'all', 'application_connect' diff --git a/coderd/database/migrations/000273_workspace_updates.down.sql b/coderd/database/migrations/000273_workspace_updates.down.sql new file mode 100644 index 0000000000000..b7c80319a06b1 --- /dev/null +++ b/coderd/database/migrations/000273_workspace_updates.down.sql @@ -0,0 +1 @@ +DROP TYPE agent_id_name_pair; diff --git a/coderd/database/migrations/000273_workspace_updates.up.sql b/coderd/database/migrations/000273_workspace_updates.up.sql new file mode 100644 index 0000000000000..bca44908cc71e --- /dev/null +++ b/coderd/database/migrations/000273_workspace_updates.up.sql @@ -0,0 +1,4 @@ +CREATE TYPE agent_id_name_pair AS ( + id uuid, + name text +); diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 9cab04d8e5c2e..e687994778017 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -221,6 +221,7 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([ type workspaceQuerier interface { GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) + GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, prepared rbac.PreparedAuthorized) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. @@ -320,6 +321,49 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa return items, nil } +func (q *sqlQuerier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, prepared rbac.PreparedAuthorized) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) { + authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigWorkspaces()) + if err != nil { + return nil, xerrors.Errorf("compile authorized filter: %w", err) + } + + // In order to properly use ORDER BY, OFFSET, and LIMIT, we need to inject the + // authorizedFilter between the end of the where clause and those statements. + filtered, err := insertAuthorizedFilter(getWorkspacesAndAgentsByOwnerID, fmt.Sprintf(" AND %s", authorizedFilter)) + if err != nil { + return nil, xerrors.Errorf("insert authorized filter: %w", err) + } + + // The name comment is for metric tracking + query := fmt.Sprintf("-- name: GetAuthorizedWorkspacesAndAgentsByOwnerID :many\n%s", filtered) + rows, err := q.db.QueryContext(ctx, query, ownerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWorkspacesAndAgentsByOwnerIDRow + for rows.Next() { + var i GetWorkspacesAndAgentsByOwnerIDRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.JobStatus, + &i.Transition, + pq.Array(&i.Agents), + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + type userQuerier interface { GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, prepared rbac.PreparedAuthorized) ([]GetUsersRow, error) } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index fcb58a7d6e305..46d1b1ae5b322 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -345,6 +345,7 @@ type sqlcQuerier interface { // It has to be a CTE because the set returning function 'unnest' cannot // be used in a WHERE clause. GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) + GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]WorkspaceTable, error) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) // We use the organization_id as the id diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 58c9626f2c9bf..41fca8d0a453e 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -24,7 +24,9 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/migrations" + "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/testutil" ) @@ -612,6 +614,130 @@ func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) { }) } +func TestGetAuthorizedWorkspacesAndAgentsByOwnerID(t *testing.T) { + t.Parallel() + if testing.Short() { + t.SkipNow() + } + + ctx := testutil.Context(t, testutil.WaitLong) + sqlDB := testSQLDB(t) + err := migrations.Up(sqlDB) + require.NoError(t, err) + db := database.New(sqlDB) + authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()) + + org := dbgen.Organization(t, db, database.Organization{}) + owner := dbgen.User(t, db, database.User{ + RBACRoles: []string{rbac.RoleOwner().String()}, + }) + user := dbgen.User(t, db, database.User{}) + tpl := dbgen.Template(t, db, database.Template{ + OrganizationID: org.ID, + CreatedBy: owner.ID, + }) + + pendingID := uuid.New() + createTemplateVersion(t, db, tpl, tvArgs{ + Status: database.ProvisionerJobStatusPending, + CreateWorkspace: true, + WorkspaceID: pendingID, + CreateAgent: true, + }) + failedID := uuid.New() + createTemplateVersion(t, db, tpl, tvArgs{ + Status: database.ProvisionerJobStatusFailed, + CreateWorkspace: true, + CreateAgent: true, + WorkspaceID: failedID, + }) + succeededID := uuid.New() + createTemplateVersion(t, db, tpl, tvArgs{ + Status: database.ProvisionerJobStatusSucceeded, + WorkspaceTransition: database.WorkspaceTransitionStart, + CreateWorkspace: true, + WorkspaceID: succeededID, + CreateAgent: true, + ExtraAgents: 1, + ExtraBuilds: 2, + }) + deletedID := uuid.New() + createTemplateVersion(t, db, tpl, tvArgs{ + Status: database.ProvisionerJobStatusSucceeded, + WorkspaceTransition: database.WorkspaceTransitionDelete, + CreateWorkspace: true, + WorkspaceID: deletedID, + CreateAgent: false, + }) + + ownerCheckFn := func(ownerRows []database.GetWorkspacesAndAgentsByOwnerIDRow) { + require.Len(t, ownerRows, 4) + for _, row := range ownerRows { + switch row.ID { + case pendingID: + require.Len(t, row.Agents, 1) + require.Equal(t, database.ProvisionerJobStatusPending, row.JobStatus) + case failedID: + require.Len(t, row.Agents, 1) + require.Equal(t, database.ProvisionerJobStatusFailed, row.JobStatus) + case succeededID: + require.Len(t, row.Agents, 2) + require.Equal(t, database.ProvisionerJobStatusSucceeded, row.JobStatus) + require.Equal(t, database.WorkspaceTransitionStart, row.Transition) + case deletedID: + require.Len(t, row.Agents, 0) + require.Equal(t, database.ProvisionerJobStatusSucceeded, row.JobStatus) + require.Equal(t, database.WorkspaceTransitionDelete, row.Transition) + default: + t.Fatalf("unexpected workspace ID: %s", row.ID) + } + } + } + t.Run("sqlQuerier", func(t *testing.T) { + t.Parallel() + + userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.ID, rbac.ExpandableScope(rbac.ScopeAll)) + require.NoError(t, err) + preparedUser, err := authorizer.Prepare(ctx, userSubject, policy.ActionRead, rbac.ResourceWorkspace.Type) + require.NoError(t, err) + userCtx := dbauthz.As(ctx, userSubject) + userRows, err := db.GetAuthorizedWorkspacesAndAgentsByOwnerID(userCtx, owner.ID, preparedUser) + require.NoError(t, err) + require.Len(t, userRows, 0) + + ownerSubject, _, err := httpmw.UserRBACSubject(ctx, db, owner.ID, rbac.ExpandableScope(rbac.ScopeAll)) + require.NoError(t, err) + preparedOwner, err := authorizer.Prepare(ctx, ownerSubject, policy.ActionRead, rbac.ResourceWorkspace.Type) + require.NoError(t, err) + ownerCtx := dbauthz.As(ctx, ownerSubject) + ownerRows, err := db.GetAuthorizedWorkspacesAndAgentsByOwnerID(ownerCtx, owner.ID, preparedOwner) + require.NoError(t, err) + ownerCheckFn(ownerRows) + }) + + t.Run("dbauthz", func(t *testing.T) { + t.Parallel() + + authzdb := dbauthz.New(db, authorizer, slogtest.Make(t, &slogtest.Options{}), coderdtest.AccessControlStorePointer()) + + userSubject, _, err := httpmw.UserRBACSubject(ctx, authzdb, user.ID, rbac.ExpandableScope(rbac.ScopeAll)) + require.NoError(t, err) + userCtx := dbauthz.As(ctx, userSubject) + + ownerSubject, _, err := httpmw.UserRBACSubject(ctx, authzdb, owner.ID, rbac.ExpandableScope(rbac.ScopeAll)) + require.NoError(t, err) + ownerCtx := dbauthz.As(ctx, ownerSubject) + + userRows, err := authzdb.GetWorkspacesAndAgentsByOwnerID(userCtx, owner.ID) + require.NoError(t, err) + require.Len(t, userRows, 0) + + ownerRows, err := authzdb.GetWorkspacesAndAgentsByOwnerID(ownerCtx, owner.ID) + require.NoError(t, err) + ownerCheckFn(ownerRows) + }) +} + func TestInsertWorkspaceAgentLogs(t *testing.T) { t.Parallel() if testing.Short() { @@ -1537,7 +1663,11 @@ type tvArgs struct { Status database.ProvisionerJobStatus // CreateWorkspace is true if we should create a workspace for the template version CreateWorkspace bool + WorkspaceID uuid.UUID + CreateAgent bool WorkspaceTransition database.WorkspaceTransition + ExtraAgents int + ExtraBuilds int } // createTemplateVersion is a helper function to create a version with its dependencies. @@ -1554,49 +1684,18 @@ func createTemplateVersion(t testing.TB, db database.Store, tpl database.Templat CreatedBy: tpl.CreatedBy, }) - earlier := sql.NullTime{ - Time: dbtime.Now().Add(time.Second * -30), - Valid: true, - } - now := sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - j := database.ProvisionerJob{ + latestJob := database.ProvisionerJob{ ID: version.JobID, - CreatedAt: earlier.Time, - UpdatedAt: earlier.Time, Error: sql.NullString{}, OrganizationID: tpl.OrganizationID, InitiatorID: tpl.CreatedBy, Type: database.ProvisionerJobTypeTemplateVersionImport, } - - switch args.Status { - case database.ProvisionerJobStatusRunning: - j.StartedAt = earlier - case database.ProvisionerJobStatusPending: - case database.ProvisionerJobStatusFailed: - j.StartedAt = earlier - j.CompletedAt = now - j.Error = sql.NullString{ - String: "failed", - Valid: true, - } - j.ErrorCode = sql.NullString{ - String: "failed", - Valid: true, - } - case database.ProvisionerJobStatusSucceeded: - j.StartedAt = earlier - j.CompletedAt = now - default: - t.Fatalf("invalid status: %s", args.Status) - } - - dbgen.ProvisionerJob(t, db, nil, j) + setJobStatus(t, args.Status, &latestJob) + dbgen.ProvisionerJob(t, db, nil, latestJob) if args.CreateWorkspace { wrk := dbgen.Workspace(t, db, database.WorkspaceTable{ + ID: args.WorkspaceID, CreatedAt: time.Time{}, UpdatedAt: time.Time{}, OwnerID: tpl.CreatedBy, @@ -1607,11 +1706,15 @@ func createTemplateVersion(t testing.TB, db database.Store, tpl database.Templat if args.WorkspaceTransition != "" { trans = args.WorkspaceTransition } - buildJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + latestJob = database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, - CompletedAt: now, InitiatorID: tpl.CreatedBy, OrganizationID: tpl.OrganizationID, + } + setJobStatus(t, args.Status, &latestJob) + latestJob = dbgen.ProvisionerJob(t, db, nil, latestJob) + latestResource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: latestJob.ID, }) dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: wrk.ID, @@ -1619,12 +1722,77 @@ func createTemplateVersion(t testing.TB, db database.Store, tpl database.Templat BuildNumber: 1, Transition: trans, InitiatorID: tpl.CreatedBy, - JobID: buildJob.ID, + JobID: latestJob.ID, }) + for i := 0; i < args.ExtraBuilds; i++ { + latestJob = database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + InitiatorID: tpl.CreatedBy, + OrganizationID: tpl.OrganizationID, + } + setJobStatus(t, args.Status, &latestJob) + latestJob = dbgen.ProvisionerJob(t, db, nil, latestJob) + latestResource = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: latestJob.ID, + }) + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: wrk.ID, + TemplateVersionID: version.ID, + BuildNumber: int32(i) + 2, + Transition: trans, + InitiatorID: tpl.CreatedBy, + JobID: latestJob.ID, + }) + } + + if args.CreateAgent { + dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: latestResource.ID, + }) + } + for i := 0; i < args.ExtraAgents; i++ { + dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: latestResource.ID, + }) + } } return version } +func setJobStatus(t testing.TB, status database.ProvisionerJobStatus, j *database.ProvisionerJob) { + t.Helper() + + earlier := sql.NullTime{ + Time: dbtime.Now().Add(time.Second * -30), + Valid: true, + } + now := sql.NullTime{ + Time: dbtime.Now(), + Valid: true, + } + switch status { + case database.ProvisionerJobStatusRunning: + j.StartedAt = earlier + case database.ProvisionerJobStatusPending: + case database.ProvisionerJobStatusFailed: + j.StartedAt = earlier + j.CompletedAt = now + j.Error = sql.NullString{ + String: "failed", + Valid: true, + } + j.ErrorCode = sql.NullString{ + String: "failed", + Valid: true, + } + case database.ProvisionerJobStatusSucceeded: + j.StartedAt = earlier + j.CompletedAt = now + default: + t.Fatalf("invalid status: %s", status) + } +} + func TestArchiveVersions(t *testing.T) { t.Parallel() if testing.Short() { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 46928ae1d3738..e72db60f3b051 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15274,6 +15274,81 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) return items, nil } +const getWorkspacesAndAgentsByOwnerID = `-- name: GetWorkspacesAndAgentsByOwnerID :many +SELECT + workspaces.id as id, + workspaces.name as name, + job_status, + transition, + (array_agg(ROW(agent_id, agent_name)::agent_id_name_pair) FILTER (WHERE agent_id IS NOT NULL))::agent_id_name_pair[] as agents +FROM workspaces +LEFT JOIN LATERAL ( + SELECT + workspace_id, + job_id, + transition, + job_status + FROM workspace_builds + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_builds.job_id + WHERE workspace_builds.workspace_id = workspaces.id + ORDER BY build_number DESC + LIMIT 1 +) latest_build ON true +LEFT JOIN LATERAL ( + SELECT + workspace_agents.id as agent_id, + workspace_agents.name as agent_name, + job_id + FROM workspace_resources + JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id + WHERE job_id = latest_build.job_id +) resources ON true +WHERE + -- Filter by owner_id + workspaces.owner_id = $1 :: uuid + AND workspaces.deleted = false + -- Authorize Filter clause will be injected below in GetAuthorizedWorkspacesAndAgentsByOwnerID + -- @authorize_filter +GROUP BY workspaces.id, workspaces.name, latest_build.job_status, latest_build.job_id, latest_build.transition +` + +type GetWorkspacesAndAgentsByOwnerIDRow struct { + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + Agents []AgentIDNamePair `db:"agents" json:"agents"` +} + +func (q *sqlQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesAndAgentsByOwnerID, ownerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWorkspacesAndAgentsByOwnerIDRow + for rows.Next() { + var i GetWorkspacesAndAgentsByOwnerIDRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.JobStatus, + &i.Transition, + pq.Array(&i.Agents), + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 369333a5eab9d..a1f41eb84d603 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -690,3 +690,42 @@ UPDATE workspaces SET favorite = true WHERE id = @id; -- name: UnfavoriteWorkspace :exec UPDATE workspaces SET favorite = false WHERE id = @id; + +-- name: GetWorkspacesAndAgentsByOwnerID :many +SELECT + workspaces.id as id, + workspaces.name as name, + job_status, + transition, + (array_agg(ROW(agent_id, agent_name)::agent_id_name_pair) FILTER (WHERE agent_id IS NOT NULL))::agent_id_name_pair[] as agents +FROM workspaces +LEFT JOIN LATERAL ( + SELECT + workspace_id, + job_id, + transition, + job_status + FROM workspace_builds + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_builds.job_id + WHERE workspace_builds.workspace_id = workspaces.id + ORDER BY build_number DESC + LIMIT 1 +) latest_build ON true +LEFT JOIN LATERAL ( + SELECT + workspace_agents.id as agent_id, + workspace_agents.name as agent_name, + job_id + FROM workspace_resources + JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id + WHERE job_id = latest_build.job_id +) resources ON true +WHERE + -- Filter by owner_id + workspaces.owner_id = @owner_id :: uuid + AND workspaces.deleted = false + -- Authorize Filter clause will be injected below in GetAuthorizedWorkspacesAndAgentsByOwnerID + -- @authorize_filter +GROUP BY workspaces.id, workspaces.name, latest_build.job_status, latest_build.job_id, latest_build.transition; + + diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 257c95ddb2d7a..2161feb47e1c3 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -28,6 +28,9 @@ sql: emit_enum_valid_method: true emit_all_enum_values: true overrides: + - db_type: "agent_id_name_pair" + go_type: + type: "AgentIDNamePair" # Used in 'CustomRoles' query to filter by (name,organization_id) - db_type: "name_organization_pair" go_type: diff --git a/coderd/database/types.go b/coderd/database/types.go index f6cf87db14ec7..8e22258382abb 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "strings" "time" "github.com/google/uuid" @@ -174,3 +175,35 @@ func (*NameOrganizationPair) Scan(_ interface{}) error { func (a NameOrganizationPair) Value() (driver.Value, error) { return fmt.Sprintf(`(%s,%s)`, a.Name, a.OrganizationID.String()), nil } + +// AgentIDNamePair is used as a result tuple for workspace and agent rows. +type AgentIDNamePair struct { + ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` +} + +func (p *AgentIDNamePair) Scan(src interface{}) error { + var v string + switch a := src.(type) { + case []byte: + v = string(a) + case string: + v = a + default: + return xerrors.Errorf("unexpected type %T", src) + } + parts := strings.Split(strings.Trim(v, "()"), ",") + if len(parts) != 2 { + return xerrors.New("invalid format for AgentIDNamePair") + } + id, err := uuid.Parse(strings.TrimSpace(parts[0])) + if err != nil { + return err + } + p.ID, p.Name = id, strings.TrimSpace(parts[1]) + return nil +} + +func (p AgentIDNamePair) Value() (driver.Value, error) { + return fmt.Sprintf(`(%s,%s)`, p.ID.String(), p.Name), nil +} From b1298a3c1ec1ea9ea05df105bc66c69f4c7976a1 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:53:53 +1100 Subject: [PATCH 020/223] feat: add WorkspaceUpdates tailnet RPC (#14847) Closes #14716 Closes #14717 Adds a new user-scoped tailnet API endpoint (`api/v2/tailnet`) with a new RPC stream for receiving updates on workspaces owned by a specific user, as defined in #14716. When a stream is started, the `WorkspaceUpdatesProvider` will begin listening on the user-scoped pubsub events implemented in #14964. When a relevant event type is seen (such as a workspace state transition), the provider will query the DB for all the workspaces (and agents) owned by the user. This gets compared against the result of the previous query to produce a set of workspace updates. Workspace updates can be requested for any user ID, however only workspaces the authorised user is permitted to `ActionRead` will have their updates streamed. Opening a tunnel to an agent requires that the user can perform `ActionSSH` against the workspace containing it. --- coderd/apidoc/docs.go | 19 + coderd/apidoc/swagger.json | 17 + coderd/coderd.go | 23 +- coderd/database/dbfake/dbfake.go | 8 + coderd/workspaceagents.go | 140 +++- coderd/workspaceagents_test.go | 190 +++++ coderd/workspacebuilds.go | 36 +- coderd/workspaceupdates.go | 313 ++++++++ coderd/workspaceupdates_test.go | 371 +++++++++ codersdk/provisionerdaemons.go | 34 + .../workspacesdk/connector_internal_test.go | 5 + docs/reference/api/agents.md | 20 + enterprise/tailnet/connio.go | 2 +- enterprise/tailnet/pgcoord_test.go | 36 + tailnet/convert.go | 28 + tailnet/coordinator.go | 4 +- tailnet/coordinator_test.go | 49 +- tailnet/peer.go | 4 +- tailnet/proto/tailnet.pb.go | 728 ++++++++++++++---- tailnet/proto/tailnet.proto | 38 + tailnet/proto/tailnet_drpc.pb.go | 70 +- tailnet/service.go | 93 ++- tailnet/service_test.go | 186 ++++- tailnet/test/peer.go | 17 + tailnet/tunnel.go | 60 +- 25 files changed, 2220 insertions(+), 271 deletions(-) create mode 100644 coderd/workspaceupdates.go create mode 100644 coderd/workspaceupdates_test.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 372303c320a34..48b550c9ed010 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3770,6 +3770,25 @@ const docTemplate = `{ } } }, + "/tailnet": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Agents" + ], + "summary": "User-scoped tailnet RPC connection", + "operationId": "user-scoped-tailnet-rpc-connection", + "responses": { + "101": { + "description": "Switching Protocols" + } + } + } + }, "/templates": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index db8b53e966bf4..c9c79b443d3d0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3316,6 +3316,23 @@ } } }, + "/tailnet": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Agents"], + "summary": "User-scoped tailnet RPC connection", + "operationId": "user-scoped-tailnet-rpc-connection", + "responses": { + "101": { + "description": "Switching Protocols" + } + } + } + }, "/templates": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 70101b7020890..39df674fecca8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -493,6 +493,8 @@ func New(options *Options) *API { } } + updatesProvider := NewUpdatesProvider(options.Logger.Named("workspace_updates"), options.Pubsub, options.Database, options.Authorizer) + // Start a background process that rotates keys. We intentionally start this after the caches // are created to force initial requests for a key to populate the caches. This helps catch // bugs that may only occur when a key isn't precached in tests and the latency cost is minimal. @@ -523,6 +525,7 @@ func New(options *Options) *API { metricsCache: metricsCache, Auditor: atomic.Pointer[audit.Auditor]{}, TailnetCoordinator: atomic.Pointer[tailnet.Coordinator]{}, + UpdatesProvider: updatesProvider, TemplateScheduleStore: options.TemplateScheduleStore, UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore, AccessControlStore: options.AccessControlStore, @@ -652,12 +655,13 @@ func New(options *Options) *API { panic("CoordinatorResumeTokenProvider is nil") } api.TailnetClientService, err = tailnet.NewClientService(tailnet.ClientServiceOptions{ - Logger: api.Logger.Named("tailnetclient"), - CoordPtr: &api.TailnetCoordinator, - DERPMapUpdateFrequency: api.Options.DERPMapUpdateFrequency, - DERPMapFn: api.DERPMap, - NetworkTelemetryHandler: api.NetworkTelemetryBatcher.Handler, - ResumeTokenProvider: api.Options.CoordinatorResumeTokenProvider, + Logger: api.Logger.Named("tailnetclient"), + CoordPtr: &api.TailnetCoordinator, + DERPMapUpdateFrequency: api.Options.DERPMapUpdateFrequency, + DERPMapFn: api.DERPMap, + NetworkTelemetryHandler: api.NetworkTelemetryBatcher.Handler, + ResumeTokenProvider: api.Options.CoordinatorResumeTokenProvider, + WorkspaceUpdatesProvider: api.UpdatesProvider, }) if err != nil { api.Logger.Fatal(context.Background(), "failed to initialize tailnet client service", slog.Error(err)) @@ -1327,6 +1331,10 @@ func New(options *Options) *API { }) r.Get("/dispatch-methods", api.notificationDispatchMethods) }) + r.Route("/tailnet", func(r chi.Router) { + r.Use(apiKeyMiddleware) + r.Get("/", api.tailnetRPCConn) + }) }) if options.SwaggerEndpoint { @@ -1408,6 +1416,8 @@ type API struct { AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] PortSharer atomic.Pointer[portsharing.PortSharer] + UpdatesProvider tailnet.WorkspaceUpdatesProvider + HTTPAuth *HTTPAuthorizer // APIHandler serves "/api/v2" @@ -1489,6 +1499,7 @@ func (api *API) Close() error { _ = api.OIDCConvertKeyCache.Close() _ = api.AppSigningKeyCache.Close() _ = api.AppEncryptionKeyCache.Close() + _ = api.UpdatesProvider.Close() return nil } diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 3ff9f59fa138e..ca514479cab6a 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -224,6 +224,14 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse { } _ = dbgen.WorkspaceBuildParameters(b.t, b.db, b.params) + if b.ws.Deleted { + err = b.db.UpdateWorkspaceDeletedByID(ownerCtx, database.UpdateWorkspaceDeletedByIDParams{ + ID: b.ws.ID, + Deleted: true, + }) + require.NoError(b.t, err) + } + if b.ps != nil { msg, err := json.Marshal(wspubsub.WorkspaceEvent{ Kind: wspubsub.WorkspaceEventKindStateChange, diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 14e986123edb7..922d80f0e8085 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -33,6 +33,7 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" @@ -844,31 +845,10 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R return } - // Accept a resume_token query parameter to use the same peer ID. - var ( - peerID = uuid.New() - resumeToken = r.URL.Query().Get("resume_token") - ) - if resumeToken != "" { - var err error - peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(ctx, resumeToken) - // If the token is missing the key ID, it's probably an old token in which - // case we just want to generate a new peer ID. - if xerrors.Is(err, jwtutils.ErrMissingKeyID) { - peerID = uuid.New() - } else if err != nil { - httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ - Message: workspacesdk.CoordinateAPIInvalidResumeToken, - Detail: err.Error(), - Validations: []codersdk.ValidationError{ - {Field: "resume_token", Detail: workspacesdk.CoordinateAPIInvalidResumeToken}, - }, - }) - return - } else { - api.Logger.Debug(ctx, "accepted coordinate resume token for peer", - slog.F("peer_id", peerID.String())) - } + peerID, err := api.handleResumeToken(ctx, rw, r) + if err != nil { + // handleResumeToken has already written the response. + return } api.WebsocketWaitMutex.Lock() @@ -891,13 +871,47 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R go httpapi.Heartbeat(ctx, conn) defer conn.Close(websocket.StatusNormalClosure, "") - err = api.TailnetClientService.ServeClient(ctx, version, wsNetConn, peerID, workspaceAgent.ID) + err = api.TailnetClientService.ServeClient(ctx, version, wsNetConn, tailnet.StreamID{ + Name: "client", + ID: peerID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: workspaceAgent.ID, + }, + }) if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) { _ = conn.Close(websocket.StatusInternalError, err.Error()) return } } +// handleResumeToken accepts a resume_token query parameter to use the same peer ID +func (api *API) handleResumeToken(ctx context.Context, rw http.ResponseWriter, r *http.Request) (peerID uuid.UUID, err error) { + peerID = uuid.New() + resumeToken := r.URL.Query().Get("resume_token") + if resumeToken != "" { + peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(ctx, resumeToken) + // If the token is missing the key ID, it's probably an old token in which + // case we just want to generate a new peer ID. + if xerrors.Is(err, jwtutils.ErrMissingKeyID) { + peerID = uuid.New() + err = nil + } else if err != nil { + httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ + Message: workspacesdk.CoordinateAPIInvalidResumeToken, + Detail: err.Error(), + Validations: []codersdk.ValidationError{ + {Field: "resume_token", Detail: workspacesdk.CoordinateAPIInvalidResumeToken}, + }, + }) + return peerID, err + } else { + api.Logger.Debug(ctx, "accepted coordinate resume token for peer", + slog.F("peer_id", peerID.String())) + } + } + return peerID, err +} + // @Summary Post workspace agent log source // @ID post-workspace-agent-log-source // @Security CoderSessionToken @@ -1469,6 +1483,80 @@ func (api *API) workspaceAgentsExternalAuthListen(ctx context.Context, rw http.R } } +// @Summary User-scoped tailnet RPC connection +// @ID user-scoped-tailnet-rpc-connection +// @Security CoderSessionToken +// @Tags Agents +// @Success 101 +// @Router /tailnet [get] +func (api *API) tailnetRPCConn(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + version := "2.0" + qv := r.URL.Query().Get("version") + if qv != "" { + version = qv + } + if err := proto.CurrentVersion.Validate(version); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unknown or unsupported API version", + Validations: []codersdk.ValidationError{ + {Field: "version", Detail: err.Error()}, + }, + }) + return + } + + peerID, err := api.handleResumeToken(ctx, rw, r) + if err != nil { + // handleResumeToken has already written the response. + return + } + + // Used to authorize tunnel request + sshPrep, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionSSH, rbac.ResourceWorkspace.Type) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error preparing sql filter.", + Detail: err.Error(), + }) + return + } + + api.WebsocketWaitMutex.Lock() + api.WebsocketWaitGroup.Add(1) + api.WebsocketWaitMutex.Unlock() + defer api.WebsocketWaitGroup.Done() + + conn, err := websocket.Accept(rw, r, nil) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Failed to accept websocket.", + Detail: err.Error(), + }) + return + } + ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageBinary) + defer wsNetConn.Close() + defer conn.Close(websocket.StatusNormalClosure, "") + + go httpapi.Heartbeat(ctx, conn) + err = api.TailnetClientService.ServeClient(ctx, version, wsNetConn, tailnet.StreamID{ + Name: "client", + ID: peerID, + Auth: tailnet.ClientUserCoordinateeAuth{ + Auth: &rbacAuthorizer{ + sshPrep: sshPrep, + db: api.Database, + }, + }, + }) + if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) { + _ = conn.Close(websocket.StatusInternalError, err.Error()) + return + } +} + // createExternalAuthResponse creates an ExternalAuthResponse based on the // provider type. This is to support legacy `/workspaceagents/me/gitauth` // which uses `Username` and `Password`. diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index ba677975471d6..1ab2eb64b874a 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "net" "net/http" "runtime" @@ -38,6 +39,7 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/jwtutils" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -1930,6 +1932,106 @@ func TestWorkspaceAgentExternalAuthListen(t *testing.T) { }) } +func TestOwnedWorkspacesCoordinate(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + firstClient, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Coordinator: tailnet.NewCoordinator(logger), + }) + firstUser := coderdtest.CreateFirstUser(t, firstClient) + member, memberUser := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin()) + + // Create a workspace with an agent + firstWorkspace := buildWorkspaceWithAgent(t, member, firstUser.OrganizationID, memberUser.ID, api.Database, api.Pubsub) + + u, err := member.URL.Parse("/api/v2/tailnet") + require.NoError(t, err) + q := u.Query() + q.Set("version", "2.0") + u.RawQuery = q.Encode() + + //nolint:bodyclose // websocket package closes this for you + wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPHeader: http.Header{ + "Coder-Session-Token": []string{member.SessionToken()}, + }, + }) + if err != nil { + if resp.StatusCode != http.StatusSwitchingProtocols { + err = codersdk.ReadBodyAsError(resp) + } + require.NoError(t, err) + } + defer wsConn.Close(websocket.StatusNormalClosure, "done") + + rpcClient, err := tailnet.NewDRPCClient( + websocket.NetConn(ctx, wsConn, websocket.MessageBinary), + logger, + ) + require.NoError(t, err) + + stream, err := rpcClient.WorkspaceUpdates(ctx, &tailnetproto.WorkspaceUpdatesRequest{ + WorkspaceOwnerId: tailnet.UUIDToByteSlice(memberUser.ID), + }) + require.NoError(t, err) + + // First update will contain the existing workspace and agent + update, err := stream.Recv() + require.NoError(t, err) + require.Len(t, update.UpsertedWorkspaces, 1) + require.EqualValues(t, update.UpsertedWorkspaces[0].Id, firstWorkspace.ID) + require.Len(t, update.UpsertedAgents, 1) + require.EqualValues(t, update.UpsertedAgents[0].WorkspaceId, firstWorkspace.ID) + require.Len(t, update.DeletedWorkspaces, 0) + require.Len(t, update.DeletedAgents, 0) + + // Build a second workspace + secondWorkspace := buildWorkspaceWithAgent(t, member, firstUser.OrganizationID, memberUser.ID, api.Database, api.Pubsub) + + // Wait for the second workspace to be running with an agent + expectedState := map[uuid.UUID]workspace{ + secondWorkspace.ID: { + Status: tailnetproto.Workspace_RUNNING, + NumAgents: 1, + }, + } + waitForUpdates(t, ctx, stream, map[uuid.UUID]workspace{}, expectedState) + + // Wait for the workspace and agent to be deleted + secondWorkspace.Deleted = true + dbfake.WorkspaceBuild(t, api.Database, secondWorkspace). + Seed(database.WorkspaceBuild{ + Transition: database.WorkspaceTransitionDelete, + BuildNumber: 2, + }).Do() + + waitForUpdates(t, ctx, stream, expectedState, map[uuid.UUID]workspace{ + secondWorkspace.ID: { + Status: tailnetproto.Workspace_DELETED, + NumAgents: 0, + }, + }) +} + +func buildWorkspaceWithAgent( + t *testing.T, + client *codersdk.Client, + orgID uuid.UUID, + ownerID uuid.UUID, + db database.Store, + ps pubsub.Pubsub, +) database.WorkspaceTable { + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: orgID, + OwnerID: ownerID, + }).WithAgent().Pubsub(ps).Do() + _ = agenttest.New(t, client.URL, r.AgentToken) + coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() + return r.Workspace +} + func requireGetManifest(ctx context.Context, t testing.TB, aAPI agentproto.DRPCAgentClient) agentsdk.Manifest { mp, err := aAPI.GetManifest(ctx, &agentproto.GetManifestRequest{}) require.NoError(t, err) @@ -1949,3 +2051,91 @@ func postStartup(ctx context.Context, t testing.TB, client agent.Client, startup _, err = aAPI.UpdateStartup(ctx, &agentproto.UpdateStartupRequest{Startup: startup}) return err } + +type workspace struct { + Status tailnetproto.Workspace_Status + NumAgents int +} + +func waitForUpdates( + t *testing.T, + //nolint:revive // t takes precedence + ctx context.Context, + stream tailnetproto.DRPCTailnet_WorkspaceUpdatesClient, + currentState map[uuid.UUID]workspace, + expectedState map[uuid.UUID]workspace, +) { + t.Helper() + errCh := make(chan error, 1) + go func() { + for { + select { + case <-ctx.Done(): + errCh <- ctx.Err() + return + default: + } + update, err := stream.Recv() + if err != nil { + errCh <- err + return + } + for _, ws := range update.UpsertedWorkspaces { + id, err := uuid.FromBytes(ws.Id) + if err != nil { + errCh <- err + return + } + currentState[id] = workspace{ + Status: ws.Status, + NumAgents: currentState[id].NumAgents, + } + } + for _, ws := range update.DeletedWorkspaces { + id, err := uuid.FromBytes(ws.Id) + if err != nil { + errCh <- err + return + } + currentState[id] = workspace{ + Status: tailnetproto.Workspace_DELETED, + NumAgents: currentState[id].NumAgents, + } + } + for _, a := range update.UpsertedAgents { + id, err := uuid.FromBytes(a.WorkspaceId) + if err != nil { + errCh <- err + return + } + currentState[id] = workspace{ + Status: currentState[id].Status, + NumAgents: currentState[id].NumAgents + 1, + } + } + for _, a := range update.DeletedAgents { + id, err := uuid.FromBytes(a.WorkspaceId) + if err != nil { + errCh <- err + return + } + currentState[id] = workspace{ + Status: currentState[id].Status, + NumAgents: currentState[id].NumAgents - 1, + } + } + if maps.Equal(currentState, expectedState) { + errCh <- nil + return + } + } + }() + select { + case err := <-errCh: + if err != nil { + t.Fatal(err) + } + case <-ctx.Done(): + t.Fatal("Timeout waiting for desired state", currentState) + } +} diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index da785ac3a5a8a..0974d85b54d6c 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -916,7 +916,7 @@ func (api *API) convertWorkspaceBuild( MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()), Reason: codersdk.BuildReason(build.Reason), Resources: apiResources, - Status: convertWorkspaceStatus(apiJob.Status, transition), + Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition), DailyCost: build.DailyCost, }, nil } @@ -946,40 +946,6 @@ func convertWorkspaceResource(resource database.WorkspaceResource, agents []code } } -func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition codersdk.WorkspaceTransition) codersdk.WorkspaceStatus { - switch jobStatus { - case codersdk.ProvisionerJobPending: - return codersdk.WorkspaceStatusPending - case codersdk.ProvisionerJobRunning: - switch transition { - case codersdk.WorkspaceTransitionStart: - return codersdk.WorkspaceStatusStarting - case codersdk.WorkspaceTransitionStop: - return codersdk.WorkspaceStatusStopping - case codersdk.WorkspaceTransitionDelete: - return codersdk.WorkspaceStatusDeleting - } - case codersdk.ProvisionerJobSucceeded: - switch transition { - case codersdk.WorkspaceTransitionStart: - return codersdk.WorkspaceStatusRunning - case codersdk.WorkspaceTransitionStop: - return codersdk.WorkspaceStatusStopped - case codersdk.WorkspaceTransitionDelete: - return codersdk.WorkspaceStatusDeleted - } - case codersdk.ProvisionerJobCanceling: - return codersdk.WorkspaceStatusCanceling - case codersdk.ProvisionerJobCanceled: - return codersdk.WorkspaceStatusCanceled - case codersdk.ProvisionerJobFailed: - return codersdk.WorkspaceStatusFailed - } - - // return error status since we should never get here - return codersdk.WorkspaceStatusFailed -} - func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) (codersdk.WorkspaceBuildTimings, error) { provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID) if err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/coderd/workspaceupdates.go b/coderd/workspaceupdates.go new file mode 100644 index 0000000000000..630a4be49ec6b --- /dev/null +++ b/coderd/workspaceupdates.go @@ -0,0 +1,313 @@ +package coderd + +import ( + "context" + "fmt" + "sync" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/coderd/wspubsub" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/tailnet" + "github.com/coder/coder/v2/tailnet/proto" +) + +type UpdatesQuerier interface { + // GetAuthorizedWorkspacesAndAgentsByOwnerID requires a context with an actor set + GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) + GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (database.Workspace, error) +} + +type workspacesByID = map[uuid.UUID]ownedWorkspace + +type ownedWorkspace struct { + WorkspaceName string + Status proto.Workspace_Status + Agents []database.AgentIDNamePair +} + +// Equal does not compare agents +func (w ownedWorkspace) Equal(other ownedWorkspace) bool { + return w.WorkspaceName == other.WorkspaceName && + w.Status == other.Status +} + +type sub struct { + // ALways contains an actor + ctx context.Context + cancelFn context.CancelFunc + + mu sync.RWMutex + userID uuid.UUID + ch chan *proto.WorkspaceUpdate + prev workspacesByID + + db UpdatesQuerier + ps pubsub.Pubsub + logger slog.Logger + + psCancelFn func() +} + +func (s *sub) handleEvent(ctx context.Context, event wspubsub.WorkspaceEvent, err error) { + s.mu.Lock() + defer s.mu.Unlock() + + switch event.Kind { + case wspubsub.WorkspaceEventKindStateChange: + case wspubsub.WorkspaceEventKindAgentConnectionUpdate: + case wspubsub.WorkspaceEventKindAgentTimeout: + case wspubsub.WorkspaceEventKindAgentLifecycleUpdate: + default: + if err == nil { + return + } else { + // Always attempt an update if the pubsub lost connection + s.logger.Warn(ctx, "failed to handle workspace event", slog.Error(err)) + } + } + + // Use context containing actor + rows, err := s.db.GetWorkspacesAndAgentsByOwnerID(s.ctx, s.userID) + if err != nil { + s.logger.Warn(ctx, "failed to get workspaces and agents by owner ID", slog.Error(err)) + return + } + latest := convertRows(rows) + + out, updated := produceUpdate(s.prev, latest) + if !updated { + return + } + + s.prev = latest + select { + case <-s.ctx.Done(): + return + case s.ch <- out: + } +} + +func (s *sub) start(ctx context.Context) (err error) { + rows, err := s.db.GetWorkspacesAndAgentsByOwnerID(ctx, s.userID) + if err != nil { + return xerrors.Errorf("get workspaces and agents by owner ID: %w", err) + } + + latest := convertRows(rows) + initUpdate, _ := produceUpdate(workspacesByID{}, latest) + s.ch <- initUpdate + s.prev = latest + + cancel, err := s.ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(s.userID), wspubsub.HandleWorkspaceEvent(s.handleEvent)) + if err != nil { + return xerrors.Errorf("subscribe to workspace event channel: %w", err) + } + + s.psCancelFn = cancel + return nil +} + +func (s *sub) Close() error { + s.cancelFn() + + if s.psCancelFn != nil { + s.psCancelFn() + } + + close(s.ch) + return nil +} + +func (s *sub) Updates() <-chan *proto.WorkspaceUpdate { + return s.ch +} + +var _ tailnet.Subscription = (*sub)(nil) + +type updatesProvider struct { + ps pubsub.Pubsub + logger slog.Logger + db UpdatesQuerier + auth rbac.Authorizer + + ctx context.Context + cancelFn func() +} + +var _ tailnet.WorkspaceUpdatesProvider = (*updatesProvider)(nil) + +func NewUpdatesProvider( + logger slog.Logger, + ps pubsub.Pubsub, + db UpdatesQuerier, + auth rbac.Authorizer, +) tailnet.WorkspaceUpdatesProvider { + ctx, cancel := context.WithCancel(context.Background()) + out := &updatesProvider{ + auth: auth, + db: db, + ps: ps, + logger: logger, + ctx: ctx, + cancelFn: cancel, + } + return out +} + +func (u *updatesProvider) Close() error { + u.cancelFn() + return nil +} + +// Subscribe subscribes to workspace updates for a user, for the workspaces +// that user is authorized to `ActionRead` on. The provided context must have +// a dbauthz actor set. +func (u *updatesProvider) Subscribe(ctx context.Context, userID uuid.UUID) (tailnet.Subscription, error) { + actor, ok := dbauthz.ActorFromContext(ctx) + if !ok { + return nil, xerrors.Errorf("actor not found in context") + } + ctx, cancel := context.WithCancel(u.ctx) + ctx = dbauthz.As(ctx, actor) + ch := make(chan *proto.WorkspaceUpdate, 1) + sub := &sub{ + ctx: ctx, + cancelFn: cancel, + userID: userID, + ch: ch, + db: u.db, + ps: u.ps, + logger: u.logger.Named(fmt.Sprintf("workspace_updates_subscriber_%s", userID)), + prev: workspacesByID{}, + } + err := sub.start(ctx) + if err != nil { + _ = sub.Close() + return nil, err + } + + return sub, nil +} + +func produceUpdate(old, new workspacesByID) (out *proto.WorkspaceUpdate, updated bool) { + out = &proto.WorkspaceUpdate{ + UpsertedWorkspaces: []*proto.Workspace{}, + UpsertedAgents: []*proto.Agent{}, + DeletedWorkspaces: []*proto.Workspace{}, + DeletedAgents: []*proto.Agent{}, + } + + for wsID, newWorkspace := range new { + oldWorkspace, exists := old[wsID] + // Upsert both workspace and agents if the workspace is new + if !exists { + out.UpsertedWorkspaces = append(out.UpsertedWorkspaces, &proto.Workspace{ + Id: tailnet.UUIDToByteSlice(wsID), + Name: newWorkspace.WorkspaceName, + Status: newWorkspace.Status, + }) + for _, agent := range newWorkspace.Agents { + out.UpsertedAgents = append(out.UpsertedAgents, &proto.Agent{ + Id: tailnet.UUIDToByteSlice(agent.ID), + Name: agent.Name, + WorkspaceId: tailnet.UUIDToByteSlice(wsID), + }) + } + updated = true + continue + } + // Upsert workspace if the workspace is updated + if !newWorkspace.Equal(oldWorkspace) { + out.UpsertedWorkspaces = append(out.UpsertedWorkspaces, &proto.Workspace{ + Id: tailnet.UUIDToByteSlice(wsID), + Name: newWorkspace.WorkspaceName, + Status: newWorkspace.Status, + }) + updated = true + } + + add, remove := slice.SymmetricDifference(oldWorkspace.Agents, newWorkspace.Agents) + for _, agent := range add { + out.UpsertedAgents = append(out.UpsertedAgents, &proto.Agent{ + Id: tailnet.UUIDToByteSlice(agent.ID), + Name: agent.Name, + WorkspaceId: tailnet.UUIDToByteSlice(wsID), + }) + updated = true + } + for _, agent := range remove { + out.DeletedAgents = append(out.DeletedAgents, &proto.Agent{ + Id: tailnet.UUIDToByteSlice(agent.ID), + Name: agent.Name, + WorkspaceId: tailnet.UUIDToByteSlice(wsID), + }) + updated = true + } + } + + // Delete workspace and agents if the workspace is deleted + for wsID, oldWorkspace := range old { + if _, exists := new[wsID]; !exists { + out.DeletedWorkspaces = append(out.DeletedWorkspaces, &proto.Workspace{ + Id: tailnet.UUIDToByteSlice(wsID), + Name: oldWorkspace.WorkspaceName, + Status: oldWorkspace.Status, + }) + for _, agent := range oldWorkspace.Agents { + out.DeletedAgents = append(out.DeletedAgents, &proto.Agent{ + Id: tailnet.UUIDToByteSlice(agent.ID), + Name: agent.Name, + WorkspaceId: tailnet.UUIDToByteSlice(wsID), + }) + } + updated = true + } + } + + return out, updated +} + +func convertRows(rows []database.GetWorkspacesAndAgentsByOwnerIDRow) workspacesByID { + out := workspacesByID{} + for _, row := range rows { + agents := []database.AgentIDNamePair{} + for _, agent := range row.Agents { + agents = append(agents, database.AgentIDNamePair{ + ID: agent.ID, + Name: agent.Name, + }) + } + out[row.ID] = ownedWorkspace{ + WorkspaceName: row.Name, + Status: tailnet.WorkspaceStatusToProto(codersdk.ConvertWorkspaceStatus(codersdk.ProvisionerJobStatus(row.JobStatus), codersdk.WorkspaceTransition(row.Transition))), + Agents: agents, + } + } + return out +} + +type rbacAuthorizer struct { + sshPrep rbac.PreparedAuthorized + db UpdatesQuerier +} + +func (r *rbacAuthorizer) AuthorizeTunnel(ctx context.Context, agentID uuid.UUID) error { + ws, err := r.db.GetWorkspaceByAgentID(ctx, agentID) + if err != nil { + return xerrors.Errorf("get workspace by agent ID: %w", err) + } + // Authorizes against `ActionSSH` + return r.sshPrep.Authorize(ctx, ws.RBACObject()) +} + +var _ tailnet.TunnelAuthorizer = (*rbacAuthorizer)(nil) diff --git a/coderd/workspaceupdates_test.go b/coderd/workspaceupdates_test.go new file mode 100644 index 0000000000000..7c01e6611f873 --- /dev/null +++ b/coderd/workspaceupdates_test.go @@ -0,0 +1,371 @@ +package coderd_test + +import ( + "context" + "encoding/json" + "slices" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/wspubsub" + "github.com/coder/coder/v2/tailnet" + "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/coder/v2/testutil" +) + +func TestWorkspaceUpdates(t *testing.T) { + t.Parallel() + + ws1ID := uuid.UUID{0x01} + ws1IDSlice := tailnet.UUIDToByteSlice(ws1ID) + agent1ID := uuid.UUID{0x02} + agent1IDSlice := tailnet.UUIDToByteSlice(agent1ID) + ws2ID := uuid.UUID{0x03} + ws2IDSlice := tailnet.UUIDToByteSlice(ws2ID) + ws3ID := uuid.UUID{0x04} + ws3IDSlice := tailnet.UUIDToByteSlice(ws3ID) + agent2ID := uuid.UUID{0x05} + agent2IDSlice := tailnet.UUIDToByteSlice(agent2ID) + ws4ID := uuid.UUID{0x06} + ws4IDSlice := tailnet.UUIDToByteSlice(ws4ID) + agent3ID := uuid.UUID{0x07} + agent3IDSlice := tailnet.UUIDToByteSlice(agent3ID) + + ownerID := uuid.UUID{0x08} + memberRole, err := rbac.RoleByName(rbac.RoleMember()) + require.NoError(t, err) + ownerSubject := rbac.Subject{ + FriendlyName: "member", + ID: ownerID.String(), + Roles: rbac.Roles{memberRole}, + Scope: rbac.ScopeAll, + } + + t.Run("Basic", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + db := &mockWorkspaceStore{ + orderedRows: []database.GetWorkspacesAndAgentsByOwnerIDRow{ + // Gains agent2 + { + ID: ws1ID, + Name: "ws1", + JobStatus: database.ProvisionerJobStatusRunning, + Transition: database.WorkspaceTransitionStart, + Agents: []database.AgentIDNamePair{ + { + ID: agent1ID, + Name: "agent1", + }, + }, + }, + // Changes status + { + ID: ws2ID, + Name: "ws2", + JobStatus: database.ProvisionerJobStatusRunning, + Transition: database.WorkspaceTransitionStart, + }, + // Is deleted + { + ID: ws3ID, + Name: "ws3", + JobStatus: database.ProvisionerJobStatusSucceeded, + Transition: database.WorkspaceTransitionStop, + Agents: []database.AgentIDNamePair{ + { + ID: agent3ID, + Name: "agent3", + }, + }, + }, + }, + } + + ps := &mockPubsub{ + cbs: map[string]pubsub.ListenerWithErr{}, + } + + updateProvider := coderd.NewUpdatesProvider(slogtest.Make(t, nil), ps, db, &mockAuthorizer{}) + t.Cleanup(func() { + _ = updateProvider.Close() + }) + + sub, err := updateProvider.Subscribe(dbauthz.As(ctx, ownerSubject), ownerID) + require.NoError(t, err) + t.Cleanup(func() { + _ = sub.Close() + }) + + update := testutil.RequireRecvCtx(ctx, t, sub.Updates()) + slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { + return strings.Compare(a.Name, b.Name) + }) + slices.SortFunc(update.UpsertedAgents, func(a, b *proto.Agent) int { + return strings.Compare(a.Name, b.Name) + }) + require.Equal(t, &proto.WorkspaceUpdate{ + UpsertedWorkspaces: []*proto.Workspace{ + { + Id: ws1IDSlice, + Name: "ws1", + Status: proto.Workspace_STARTING, + }, + { + Id: ws2IDSlice, + Name: "ws2", + Status: proto.Workspace_STARTING, + }, + { + Id: ws3IDSlice, + Name: "ws3", + Status: proto.Workspace_STOPPED, + }, + }, + UpsertedAgents: []*proto.Agent{ + { + Id: agent1IDSlice, + Name: "agent1", + WorkspaceId: ws1IDSlice, + }, + { + Id: agent3IDSlice, + Name: "agent3", + WorkspaceId: ws3IDSlice, + }, + }, + DeletedWorkspaces: []*proto.Workspace{}, + DeletedAgents: []*proto.Agent{}, + }, update) + + // Update the database + db.orderedRows = []database.GetWorkspacesAndAgentsByOwnerIDRow{ + { + ID: ws1ID, + Name: "ws1", + JobStatus: database.ProvisionerJobStatusRunning, + Transition: database.WorkspaceTransitionStart, + Agents: []database.AgentIDNamePair{ + { + ID: agent1ID, + Name: "agent1", + }, + { + ID: agent2ID, + Name: "agent2", + }, + }, + }, + { + ID: ws2ID, + Name: "ws2", + JobStatus: database.ProvisionerJobStatusRunning, + Transition: database.WorkspaceTransitionStop, + }, + { + ID: ws4ID, + Name: "ws4", + JobStatus: database.ProvisionerJobStatusRunning, + Transition: database.WorkspaceTransitionStart, + }, + } + publishWorkspaceEvent(t, ps, ownerID, &wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindStateChange, + WorkspaceID: ws1ID, + }) + + update = testutil.RequireRecvCtx(ctx, t, sub.Updates()) + slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { + return strings.Compare(a.Name, b.Name) + }) + require.Equal(t, &proto.WorkspaceUpdate{ + UpsertedWorkspaces: []*proto.Workspace{ + { + // Changed status + Id: ws2IDSlice, + Name: "ws2", + Status: proto.Workspace_STOPPING, + }, + { + // New workspace + Id: ws4IDSlice, + Name: "ws4", + Status: proto.Workspace_STARTING, + }, + }, + UpsertedAgents: []*proto.Agent{ + { + Id: agent2IDSlice, + Name: "agent2", + WorkspaceId: ws1IDSlice, + }, + }, + DeletedWorkspaces: []*proto.Workspace{ + { + Id: ws3IDSlice, + Name: "ws3", + Status: proto.Workspace_STOPPED, + }, + }, + DeletedAgents: []*proto.Agent{ + { + Id: agent3IDSlice, + Name: "agent3", + WorkspaceId: ws3IDSlice, + }, + }, + }, update) + }) + + t.Run("Resubscribe", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + db := &mockWorkspaceStore{ + orderedRows: []database.GetWorkspacesAndAgentsByOwnerIDRow{ + { + ID: ws1ID, + Name: "ws1", + JobStatus: database.ProvisionerJobStatusRunning, + Transition: database.WorkspaceTransitionStart, + Agents: []database.AgentIDNamePair{ + { + ID: agent1ID, + Name: "agent1", + }, + }, + }, + }, + } + + ps := &mockPubsub{ + cbs: map[string]pubsub.ListenerWithErr{}, + } + + updateProvider := coderd.NewUpdatesProvider(slogtest.Make(t, nil), ps, db, &mockAuthorizer{}) + t.Cleanup(func() { + _ = updateProvider.Close() + }) + + sub, err := updateProvider.Subscribe(dbauthz.As(ctx, ownerSubject), ownerID) + require.NoError(t, err) + t.Cleanup(func() { + _ = sub.Close() + }) + + expected := &proto.WorkspaceUpdate{ + UpsertedWorkspaces: []*proto.Workspace{ + { + Id: ws1IDSlice, + Name: "ws1", + Status: proto.Workspace_STARTING, + }, + }, + UpsertedAgents: []*proto.Agent{ + { + Id: agent1IDSlice, + Name: "agent1", + WorkspaceId: ws1IDSlice, + }, + }, + DeletedWorkspaces: []*proto.Workspace{}, + DeletedAgents: []*proto.Agent{}, + } + + update := testutil.RequireRecvCtx(ctx, t, sub.Updates()) + slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { + return strings.Compare(a.Name, b.Name) + }) + require.Equal(t, expected, update) + + resub, err := updateProvider.Subscribe(dbauthz.As(ctx, ownerSubject), ownerID) + require.NoError(t, err) + t.Cleanup(func() { + _ = resub.Close() + }) + + update = testutil.RequireRecvCtx(ctx, t, resub.Updates()) + slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { + return strings.Compare(a.Name, b.Name) + }) + require.Equal(t, expected, update) + }) +} + +func publishWorkspaceEvent(t *testing.T, ps pubsub.Pubsub, ownerID uuid.UUID, event *wspubsub.WorkspaceEvent) { + msg, err := json.Marshal(event) + require.NoError(t, err) + ps.Publish(wspubsub.WorkspaceEventChannel(ownerID), msg) +} + +type mockWorkspaceStore struct { + orderedRows []database.GetWorkspacesAndAgentsByOwnerIDRow +} + +// GetAuthorizedWorkspacesAndAgentsByOwnerID implements coderd.UpdatesQuerier. +func (m *mockWorkspaceStore) GetWorkspacesAndAgentsByOwnerID(context.Context, uuid.UUID) ([]database.GetWorkspacesAndAgentsByOwnerIDRow, error) { + return m.orderedRows, nil +} + +// GetWorkspaceByAgentID implements coderd.UpdatesQuerier. +func (*mockWorkspaceStore) GetWorkspaceByAgentID(context.Context, uuid.UUID) (database.Workspace, error) { + return database.Workspace{}, nil +} + +var _ coderd.UpdatesQuerier = (*mockWorkspaceStore)(nil) + +type mockPubsub struct { + cbs map[string]pubsub.ListenerWithErr +} + +// Close implements pubsub.Pubsub. +func (*mockPubsub) Close() error { + panic("unimplemented") +} + +// Publish implements pubsub.Pubsub. +func (m *mockPubsub) Publish(event string, message []byte) error { + cb, ok := m.cbs[event] + if !ok { + return nil + } + cb(context.Background(), message, nil) + return nil +} + +func (*mockPubsub) Subscribe(string, pubsub.Listener) (cancel func(), err error) { + panic("unimplemented") +} + +func (m *mockPubsub) SubscribeWithErr(event string, listener pubsub.ListenerWithErr) (func(), error) { + m.cbs[event] = listener + return func() {}, nil +} + +var _ pubsub.Pubsub = (*mockPubsub)(nil) + +type mockAuthorizer struct{} + +func (*mockAuthorizer) Authorize(context.Context, rbac.Subject, policy.Action, rbac.Object) error { + return nil +} + +// Prepare implements rbac.Authorizer. +func (*mockAuthorizer) Prepare(context.Context, rbac.Subject, policy.Action, string) (rbac.PreparedAuthorized, error) { + return nil, nil +} + +var _ rbac.Authorizer = (*mockAuthorizer)(nil) diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 7ba10539b671c..7b14afbbb285a 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -402,3 +402,37 @@ func (c *Client) DeleteProvisionerKey(ctx context.Context, organizationID uuid.U } return nil } + +func ConvertWorkspaceStatus(jobStatus ProvisionerJobStatus, transition WorkspaceTransition) WorkspaceStatus { + switch jobStatus { + case ProvisionerJobPending: + return WorkspaceStatusPending + case ProvisionerJobRunning: + switch transition { + case WorkspaceTransitionStart: + return WorkspaceStatusStarting + case WorkspaceTransitionStop: + return WorkspaceStatusStopping + case WorkspaceTransitionDelete: + return WorkspaceStatusDeleting + } + case ProvisionerJobSucceeded: + switch transition { + case WorkspaceTransitionStart: + return WorkspaceStatusRunning + case WorkspaceTransitionStop: + return WorkspaceStatusStopped + case WorkspaceTransitionDelete: + return WorkspaceStatusDeleted + } + case ProvisionerJobCanceling: + return WorkspaceStatusCanceling + case ProvisionerJobCanceled: + return WorkspaceStatusCanceled + case ProvisionerJobFailed: + return WorkspaceStatusFailed + } + + // return error status since we should never get here + return WorkspaceStatusFailed +} diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 19f1930c89bc5..009de5c6bfb4a 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -580,6 +580,11 @@ func (f *fakeDRPCClient) RefreshResumeToken(_ context.Context, _ *proto.RefreshR }, nil } +// WorkspaceUpdates implements proto.DRPCTailnetClient. +func (*fakeDRPCClient) WorkspaceUpdates(context.Context, *proto.WorkspaceUpdatesRequest) (proto.DRPCTailnet_WorkspaceUpdatesClient, error) { + panic("unimplemented") +} + type fakeDRPCConn struct{} var _ drpc.Conn = &fakeDRPCConn{} diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 8e7f46bc7d366..6ccffeb82305d 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -20,6 +20,26 @@ curl -X GET http://coder-server:8080/api/v2/derp-map \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## User-scoped tailnet RPC connection + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/tailnet \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /tailnet` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------------------------ | ------------------- | ------ | +| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Authenticate agent on AWS instance ### Code samples diff --git a/enterprise/tailnet/connio.go b/enterprise/tailnet/connio.go index fd2c99bdeb8eb..923af4bee080d 100644 --- a/enterprise/tailnet/connio.go +++ b/enterprise/tailnet/connio.go @@ -133,7 +133,7 @@ var errDisconnect = xerrors.New("graceful disconnect") func (c *connIO) handleRequest(req *proto.CoordinateRequest) error { c.logger.Debug(c.peerCtx, "got request") - err := c.auth.Authorize(req) + err := c.auth.Authorize(c.peerCtx, req) if err != nil { c.logger.Warn(c.peerCtx, "unauthorized request", slog.Error(err)) return xerrors.Errorf("authorize request: %w", err) diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go index 08c0017a2d1bd..c0d122aa74992 100644 --- a/enterprise/tailnet/pgcoord_test.go +++ b/enterprise/tailnet/pgcoord_test.go @@ -913,6 +913,42 @@ func TestPGCoordinatorDual_PeerReconnect(t *testing.T) { p2.AssertNeverUpdateKind(p1.ID, proto.CoordinateResponse_PeerUpdate_DISCONNECTED) } +// TestPGCoordinatorPropogatedPeerContext tests that the context for a specific peer +// is propogated through to the `Authorize` method of the coordinatee auth +func TestPGCoordinatorPropogatedPeerContext(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("test only with postgres") + } + + ctx := testutil.Context(t, testutil.WaitShort) + store, ps := dbtestutil.NewDB(t) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + peerCtx := context.WithValue(ctx, agpltest.FakeSubjectKey{}, struct{}{}) + peerID := uuid.UUID{0x01} + agentID := uuid.UUID{0x02} + + c1, err := tailnet.NewPGCoord(ctx, logger, ps, store) + require.NoError(t, err) + defer func() { + err := c1.Close() + require.NoError(t, err) + }() + + ch := make(chan struct{}) + auth := agpltest.FakeCoordinateeAuth{ + Chan: ch, + } + + reqs, _ := c1.Coordinate(peerCtx, peerID, "peer1", auth) + + testutil.RequireSendCtx(ctx, t, reqs, &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: agpl.UUIDToByteSlice(agentID)}}) + + _ = testutil.RequireRecvCtx(ctx, t, ch) +} + func assertEventuallyStatus(ctx context.Context, t *testing.T, store database.Store, agentID uuid.UUID, status database.TailnetStatus) { t.Helper() assert.Eventually(t, func() bool { diff --git a/tailnet/convert.go b/tailnet/convert.go index a7d224dc01bd0..3ba97e443fb38 100644 --- a/tailnet/convert.go +++ b/tailnet/convert.go @@ -9,6 +9,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/key" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet/proto" ) @@ -270,3 +271,30 @@ func DERPNodeFromProto(node *proto.DERPMap_Region_Node) *tailcfg.DERPNode { CanPort80: node.CanPort_80, } } + +func WorkspaceStatusToProto(status codersdk.WorkspaceStatus) proto.Workspace_Status { + switch status { + case codersdk.WorkspaceStatusCanceled: + return proto.Workspace_CANCELED + case codersdk.WorkspaceStatusCanceling: + return proto.Workspace_CANCELING + case codersdk.WorkspaceStatusDeleted: + return proto.Workspace_DELETED + case codersdk.WorkspaceStatusDeleting: + return proto.Workspace_DELETING + case codersdk.WorkspaceStatusFailed: + return proto.Workspace_FAILED + case codersdk.WorkspaceStatusPending: + return proto.Workspace_PENDING + case codersdk.WorkspaceStatusRunning: + return proto.Workspace_RUNNING + case codersdk.WorkspaceStatusStarting: + return proto.Workspace_STARTING + case codersdk.WorkspaceStatusStopped: + return proto.Workspace_STOPPED + case codersdk.WorkspaceStatusStopping: + return proto.Workspace_STOPPING + default: + return proto.Workspace_UNKNOWN + } +} diff --git a/tailnet/coordinator.go b/tailnet/coordinator.go index 54ce868df9316..b0592598959f3 100644 --- a/tailnet/coordinator.go +++ b/tailnet/coordinator.go @@ -566,7 +566,7 @@ func (c *core) node(id uuid.UUID) *Node { return v1Node } -func (c *core) handleRequest(p *peer, req *proto.CoordinateRequest) error { +func (c *core) handleRequest(ctx context.Context, p *peer, req *proto.CoordinateRequest) error { c.mutex.Lock() defer c.mutex.Unlock() if c.closed { @@ -577,7 +577,7 @@ func (c *core) handleRequest(p *peer, req *proto.CoordinateRequest) error { return ErrAlreadyRemoved } - if err := pr.auth.Authorize(req); err != nil { + if err := pr.auth.Authorize(ctx, req); err != nil { return xerrors.Errorf("authorize request: %w", err) } diff --git a/tailnet/coordinator_test.go b/tailnet/coordinator_test.go index 5ffffde8249a4..b3a803cd6aaf6 100644 --- a/tailnet/coordinator_test.go +++ b/tailnet/coordinator_test.go @@ -328,7 +328,13 @@ func TestRemoteCoordination(t *testing.T) { serveErr := make(chan error, 1) go func() { - err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, clientID, agentID) + err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: agentID, + }, + }) serveErr <- err }() @@ -377,7 +383,13 @@ func TestRemoteCoordination_SendsReadyForHandshake(t *testing.T) { serveErr := make(chan error, 1) go func() { - err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, clientID, agentID) + err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: agentID, + }, + }) serveErr <- err }() @@ -517,3 +529,36 @@ func (f *fakeCoordinatee) SetNodeCallback(callback func(*tailnet.Node)) { defer f.Unlock() f.callback = callback } + +// TestCoordinatorPropogatedPeerContext tests that the context for a specific peer +// is propogated through to the `Authorize“ method of the coordinatee auth +func TestCoordinatorPropogatedPeerContext(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + peerCtx := context.WithValue(ctx, test.FakeSubjectKey{}, struct{}{}) + peerCtx, peerCtxCancel := context.WithCancel(peerCtx) + peerID := uuid.UUID{0x01} + agentID := uuid.UUID{0x02} + + c1 := tailnet.NewCoordinator(logger) + t.Cleanup(func() { + err := c1.Close() + require.NoError(t, err) + }) + + ch := make(chan struct{}) + auth := test.FakeCoordinateeAuth{ + Chan: ch, + } + + reqs, _ := c1.Coordinate(peerCtx, peerID, "peer1", auth) + + testutil.RequireSendCtx(ctx, t, reqs, &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: tailnet.UUIDToByteSlice(agentID)}}) + _ = testutil.RequireRecvCtx(ctx, t, ch) + // If we don't cancel the context, the coordinator close will wait until the + // peer request loop finishes, which will be after the timeout + peerCtxCancel() +} diff --git a/tailnet/peer.go b/tailnet/peer.go index eadc882f5a6d6..7d69764abe103 100644 --- a/tailnet/peer.go +++ b/tailnet/peer.go @@ -121,7 +121,7 @@ func (p *peer) storeMappingLocked( }, nil } -func (p *peer) reqLoop(ctx context.Context, logger slog.Logger, handler func(*peer, *proto.CoordinateRequest) error) { +func (p *peer) reqLoop(ctx context.Context, logger slog.Logger, handler func(context.Context, *peer, *proto.CoordinateRequest) error) { for { select { case <-ctx.Done(): @@ -133,7 +133,7 @@ func (p *peer) reqLoop(ctx context.Context, logger slog.Logger, handler func(*pe return } logger.Debug(ctx, "peerReadLoop got request") - if err := handler(p, req); err != nil { + if err := handler(ctx, p, req); err != nil { if xerrors.Is(err, ErrAlreadyRemoved) || xerrors.Is(err, ErrClosed) { return } diff --git a/tailnet/proto/tailnet.pb.go b/tailnet/proto/tailnet.pb.go index c4302954c068e..b2a03fa53f5d1 100644 --- a/tailnet/proto/tailnet.pb.go +++ b/tailnet/proto/tailnet.pb.go @@ -228,6 +228,79 @@ func (TelemetryEvent_ClientType) EnumDescriptor() ([]byte, []int) { return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9, 1} } +type Workspace_Status int32 + +const ( + Workspace_UNKNOWN Workspace_Status = 0 + Workspace_PENDING Workspace_Status = 1 + Workspace_STARTING Workspace_Status = 2 + Workspace_RUNNING Workspace_Status = 3 + Workspace_STOPPING Workspace_Status = 4 + Workspace_STOPPED Workspace_Status = 5 + Workspace_FAILED Workspace_Status = 6 + Workspace_CANCELING Workspace_Status = 7 + Workspace_CANCELED Workspace_Status = 8 + Workspace_DELETING Workspace_Status = 9 + Workspace_DELETED Workspace_Status = 10 +) + +// Enum value maps for Workspace_Status. +var ( + Workspace_Status_name = map[int32]string{ + 0: "UNKNOWN", + 1: "PENDING", + 2: "STARTING", + 3: "RUNNING", + 4: "STOPPING", + 5: "STOPPED", + 6: "FAILED", + 7: "CANCELING", + 8: "CANCELED", + 9: "DELETING", + 10: "DELETED", + } + Workspace_Status_value = map[string]int32{ + "UNKNOWN": 0, + "PENDING": 1, + "STARTING": 2, + "RUNNING": 3, + "STOPPING": 4, + "STOPPED": 5, + "FAILED": 6, + "CANCELING": 7, + "CANCELED": 8, + "DELETING": 9, + "DELETED": 10, + } +) + +func (x Workspace_Status) Enum() *Workspace_Status { + p := new(Workspace_Status) + *p = x + return p +} + +func (x Workspace_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Workspace_Status) Descriptor() protoreflect.EnumDescriptor { + return file_tailnet_proto_tailnet_proto_enumTypes[4].Descriptor() +} + +func (Workspace_Status) Type() protoreflect.EnumType { + return &file_tailnet_proto_tailnet_proto_enumTypes[4] +} + +func (x Workspace_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Workspace_Status.Descriptor instead. +func (Workspace_Status) EnumDescriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{14, 0} +} + type DERPMap struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1174,6 +1247,250 @@ func (*TelemetryResponse) Descriptor() ([]byte, []int) { return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{11} } +type WorkspaceUpdatesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkspaceOwnerId []byte `protobuf:"bytes,1,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` // UUID +} + +func (x *WorkspaceUpdatesRequest) Reset() { + *x = WorkspaceUpdatesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceUpdatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceUpdatesRequest) ProtoMessage() {} + +func (x *WorkspaceUpdatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceUpdatesRequest.ProtoReflect.Descriptor instead. +func (*WorkspaceUpdatesRequest) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{12} +} + +func (x *WorkspaceUpdatesRequest) GetWorkspaceOwnerId() []byte { + if x != nil { + return x.WorkspaceOwnerId + } + return nil +} + +type WorkspaceUpdate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UpsertedWorkspaces []*Workspace `protobuf:"bytes,1,rep,name=upserted_workspaces,json=upsertedWorkspaces,proto3" json:"upserted_workspaces,omitempty"` + UpsertedAgents []*Agent `protobuf:"bytes,2,rep,name=upserted_agents,json=upsertedAgents,proto3" json:"upserted_agents,omitempty"` + DeletedWorkspaces []*Workspace `protobuf:"bytes,3,rep,name=deleted_workspaces,json=deletedWorkspaces,proto3" json:"deleted_workspaces,omitempty"` + DeletedAgents []*Agent `protobuf:"bytes,4,rep,name=deleted_agents,json=deletedAgents,proto3" json:"deleted_agents,omitempty"` +} + +func (x *WorkspaceUpdate) Reset() { + *x = WorkspaceUpdate{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceUpdate) ProtoMessage() {} + +func (x *WorkspaceUpdate) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceUpdate.ProtoReflect.Descriptor instead. +func (*WorkspaceUpdate) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{13} +} + +func (x *WorkspaceUpdate) GetUpsertedWorkspaces() []*Workspace { + if x != nil { + return x.UpsertedWorkspaces + } + return nil +} + +func (x *WorkspaceUpdate) GetUpsertedAgents() []*Agent { + if x != nil { + return x.UpsertedAgents + } + return nil +} + +func (x *WorkspaceUpdate) GetDeletedWorkspaces() []*Workspace { + if x != nil { + return x.DeletedWorkspaces + } + return nil +} + +func (x *WorkspaceUpdate) GetDeletedAgents() []*Agent { + if x != nil { + return x.DeletedAgents + } + return nil +} + +type Workspace struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Status Workspace_Status `protobuf:"varint,3,opt,name=status,proto3,enum=coder.tailnet.v2.Workspace_Status" json:"status,omitempty"` +} + +func (x *Workspace) Reset() { + *x = Workspace{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Workspace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Workspace) ProtoMessage() {} + +func (x *Workspace) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Workspace.ProtoReflect.Descriptor instead. +func (*Workspace) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{14} +} + +func (x *Workspace) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *Workspace) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Workspace) GetStatus() Workspace_Status { + if x != nil { + return x.Status + } + return Workspace_UNKNOWN +} + +type Agent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + WorkspaceId []byte `protobuf:"bytes,3,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` // UUID +} + +func (x *Agent) Reset() { + *x = Agent{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Agent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Agent) ProtoMessage() {} + +func (x *Agent) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Agent.ProtoReflect.Descriptor instead. +func (*Agent) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{15} +} + +func (x *Agent) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *Agent) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Agent) GetWorkspaceId() []byte { + if x != nil { + return x.WorkspaceId + } + return nil +} + type DERPMap_HomeParams struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1185,7 +1502,7 @@ type DERPMap_HomeParams struct { func (x *DERPMap_HomeParams) Reset() { *x = DERPMap_HomeParams{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[12] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1198,7 +1515,7 @@ func (x *DERPMap_HomeParams) String() string { func (*DERPMap_HomeParams) ProtoMessage() {} func (x *DERPMap_HomeParams) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[12] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1554,7 @@ type DERPMap_Region struct { func (x *DERPMap_Region) Reset() { *x = DERPMap_Region{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1250,7 +1567,7 @@ func (x *DERPMap_Region) String() string { func (*DERPMap_Region) ProtoMessage() {} func (x *DERPMap_Region) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1331,7 +1648,7 @@ type DERPMap_Region_Node struct { func (x *DERPMap_Region_Node) Reset() { *x = DERPMap_Region_Node{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1344,7 +1661,7 @@ func (x *DERPMap_Region_Node) String() string { func (*DERPMap_Region_Node) ProtoMessage() {} func (x *DERPMap_Region_Node) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1462,7 +1779,7 @@ type CoordinateRequest_UpdateSelf struct { func (x *CoordinateRequest_UpdateSelf) Reset() { *x = CoordinateRequest_UpdateSelf{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1475,7 +1792,7 @@ func (x *CoordinateRequest_UpdateSelf) String() string { func (*CoordinateRequest_UpdateSelf) ProtoMessage() {} func (x *CoordinateRequest_UpdateSelf) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1507,7 +1824,7 @@ type CoordinateRequest_Disconnect struct { func (x *CoordinateRequest_Disconnect) Reset() { *x = CoordinateRequest_Disconnect{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1520,7 +1837,7 @@ func (x *CoordinateRequest_Disconnect) String() string { func (*CoordinateRequest_Disconnect) ProtoMessage() {} func (x *CoordinateRequest_Disconnect) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1547,7 +1864,7 @@ type CoordinateRequest_Tunnel struct { func (x *CoordinateRequest_Tunnel) Reset() { *x = CoordinateRequest_Tunnel{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1560,7 +1877,7 @@ func (x *CoordinateRequest_Tunnel) String() string { func (*CoordinateRequest_Tunnel) ProtoMessage() {} func (x *CoordinateRequest_Tunnel) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1598,7 +1915,7 @@ type CoordinateRequest_ReadyForHandshake struct { func (x *CoordinateRequest_ReadyForHandshake) Reset() { *x = CoordinateRequest_ReadyForHandshake{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[22] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1611,7 +1928,7 @@ func (x *CoordinateRequest_ReadyForHandshake) String() string { func (*CoordinateRequest_ReadyForHandshake) ProtoMessage() {} func (x *CoordinateRequest_ReadyForHandshake) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[22] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1648,7 +1965,7 @@ type CoordinateResponse_PeerUpdate struct { func (x *CoordinateResponse_PeerUpdate) Reset() { *x = CoordinateResponse_PeerUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[23] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1661,7 +1978,7 @@ func (x *CoordinateResponse_PeerUpdate) String() string { func (*CoordinateResponse_PeerUpdate) ProtoMessage() {} func (x *CoordinateResponse_PeerUpdate) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[23] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1717,7 +2034,7 @@ type Netcheck_NetcheckIP struct { func (x *Netcheck_NetcheckIP) Reset() { *x = Netcheck_NetcheckIP{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1730,7 +2047,7 @@ func (x *Netcheck_NetcheckIP) String() string { func (*Netcheck_NetcheckIP) ProtoMessage() {} func (x *Netcheck_NetcheckIP) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1773,7 +2090,7 @@ type TelemetryEvent_P2PEndpoint struct { func (x *TelemetryEvent_P2PEndpoint) Reset() { *x = TelemetryEvent_P2PEndpoint{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[27] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1786,7 +2103,7 @@ func (x *TelemetryEvent_P2PEndpoint) String() string { func (*TelemetryEvent_P2PEndpoint) ProtoMessage() {} func (x *TelemetryEvent_P2PEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[27] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2171,35 +2488,86 @@ var file_tailnet_proto_tailnet_proto_rawDesc = []byte{ 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x89, 0x03, 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6c, - 0x6e, 0x65, 0x74, 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x6c, 0x65, 0x6d, - 0x65, 0x74, 0x72, 0x79, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, - 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, - 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x12, - 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, - 0x4d, 0x61, 0x70, 0x30, 0x01, 0x12, 0x6f, 0x0a, 0x12, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, - 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x0a, 0x17, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, + 0x22, 0xad, 0x02, 0x0a, 0x0f, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x13, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, + 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x12, + 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x73, 0x12, 0x40, 0x0a, 0x0f, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x52, 0x0e, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x4a, 0x0a, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x11, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, + 0x12, 0x3e, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, + 0x22, 0x8a, 0x02, 0x0a, 0x09, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, + 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x9c, + 0x01, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, + 0x47, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, + 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x0c, + 0x0a, 0x08, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, + 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, + 0x4c, 0x45, 0x44, 0x10, 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x49, + 0x4e, 0x47, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, + 0x10, 0x08, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x09, + 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x0a, 0x22, 0x4e, 0x0a, + 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x32, 0xed, 0x03, + 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x6f, 0x73, + 0x74, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, + 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, + 0x50, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, + 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x30, 0x01, 0x12, 0x6f, 0x0a, 0x12, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x66, 0x72, - 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, - 0x6e, 0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, - 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, - 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, - 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, - 0x2f, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, + 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0a, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, + 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x62, 0x0a, 0x10, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x29, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x30, 0x01, 0x42, 0x29, 0x5a, + 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x74, 0x61, 0x69, 0x6c, 0x6e, + 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2214,107 +2582,119 @@ func file_tailnet_proto_tailnet_proto_rawDescGZIP() []byte { return file_tailnet_proto_tailnet_proto_rawDescData } -var file_tailnet_proto_tailnet_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_tailnet_proto_tailnet_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_tailnet_proto_tailnet_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_tailnet_proto_tailnet_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_tailnet_proto_tailnet_proto_goTypes = []interface{}{ (CoordinateResponse_PeerUpdate_Kind)(0), // 0: coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind (IPFields_IPClass)(0), // 1: coder.tailnet.v2.IPFields.IPClass (TelemetryEvent_Status)(0), // 2: coder.tailnet.v2.TelemetryEvent.Status (TelemetryEvent_ClientType)(0), // 3: coder.tailnet.v2.TelemetryEvent.ClientType - (*DERPMap)(nil), // 4: coder.tailnet.v2.DERPMap - (*StreamDERPMapsRequest)(nil), // 5: coder.tailnet.v2.StreamDERPMapsRequest - (*Node)(nil), // 6: coder.tailnet.v2.Node - (*RefreshResumeTokenRequest)(nil), // 7: coder.tailnet.v2.RefreshResumeTokenRequest - (*RefreshResumeTokenResponse)(nil), // 8: coder.tailnet.v2.RefreshResumeTokenResponse - (*CoordinateRequest)(nil), // 9: coder.tailnet.v2.CoordinateRequest - (*CoordinateResponse)(nil), // 10: coder.tailnet.v2.CoordinateResponse - (*IPFields)(nil), // 11: coder.tailnet.v2.IPFields - (*Netcheck)(nil), // 12: coder.tailnet.v2.Netcheck - (*TelemetryEvent)(nil), // 13: coder.tailnet.v2.TelemetryEvent - (*TelemetryRequest)(nil), // 14: coder.tailnet.v2.TelemetryRequest - (*TelemetryResponse)(nil), // 15: coder.tailnet.v2.TelemetryResponse - (*DERPMap_HomeParams)(nil), // 16: coder.tailnet.v2.DERPMap.HomeParams - (*DERPMap_Region)(nil), // 17: coder.tailnet.v2.DERPMap.Region - nil, // 18: coder.tailnet.v2.DERPMap.RegionsEntry - nil, // 19: coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry - (*DERPMap_Region_Node)(nil), // 20: coder.tailnet.v2.DERPMap.Region.Node - nil, // 21: coder.tailnet.v2.Node.DerpLatencyEntry - nil, // 22: coder.tailnet.v2.Node.DerpForcedWebsocketEntry - (*CoordinateRequest_UpdateSelf)(nil), // 23: coder.tailnet.v2.CoordinateRequest.UpdateSelf - (*CoordinateRequest_Disconnect)(nil), // 24: coder.tailnet.v2.CoordinateRequest.Disconnect - (*CoordinateRequest_Tunnel)(nil), // 25: coder.tailnet.v2.CoordinateRequest.Tunnel - (*CoordinateRequest_ReadyForHandshake)(nil), // 26: coder.tailnet.v2.CoordinateRequest.ReadyForHandshake - (*CoordinateResponse_PeerUpdate)(nil), // 27: coder.tailnet.v2.CoordinateResponse.PeerUpdate - nil, // 28: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry - nil, // 29: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry - (*Netcheck_NetcheckIP)(nil), // 30: coder.tailnet.v2.Netcheck.NetcheckIP - (*TelemetryEvent_P2PEndpoint)(nil), // 31: coder.tailnet.v2.TelemetryEvent.P2PEndpoint - (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 33: google.protobuf.Duration - (*wrapperspb.BoolValue)(nil), // 34: google.protobuf.BoolValue - (*wrapperspb.FloatValue)(nil), // 35: google.protobuf.FloatValue + (Workspace_Status)(0), // 4: coder.tailnet.v2.Workspace.Status + (*DERPMap)(nil), // 5: coder.tailnet.v2.DERPMap + (*StreamDERPMapsRequest)(nil), // 6: coder.tailnet.v2.StreamDERPMapsRequest + (*Node)(nil), // 7: coder.tailnet.v2.Node + (*RefreshResumeTokenRequest)(nil), // 8: coder.tailnet.v2.RefreshResumeTokenRequest + (*RefreshResumeTokenResponse)(nil), // 9: coder.tailnet.v2.RefreshResumeTokenResponse + (*CoordinateRequest)(nil), // 10: coder.tailnet.v2.CoordinateRequest + (*CoordinateResponse)(nil), // 11: coder.tailnet.v2.CoordinateResponse + (*IPFields)(nil), // 12: coder.tailnet.v2.IPFields + (*Netcheck)(nil), // 13: coder.tailnet.v2.Netcheck + (*TelemetryEvent)(nil), // 14: coder.tailnet.v2.TelemetryEvent + (*TelemetryRequest)(nil), // 15: coder.tailnet.v2.TelemetryRequest + (*TelemetryResponse)(nil), // 16: coder.tailnet.v2.TelemetryResponse + (*WorkspaceUpdatesRequest)(nil), // 17: coder.tailnet.v2.WorkspaceUpdatesRequest + (*WorkspaceUpdate)(nil), // 18: coder.tailnet.v2.WorkspaceUpdate + (*Workspace)(nil), // 19: coder.tailnet.v2.Workspace + (*Agent)(nil), // 20: coder.tailnet.v2.Agent + (*DERPMap_HomeParams)(nil), // 21: coder.tailnet.v2.DERPMap.HomeParams + (*DERPMap_Region)(nil), // 22: coder.tailnet.v2.DERPMap.Region + nil, // 23: coder.tailnet.v2.DERPMap.RegionsEntry + nil, // 24: coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry + (*DERPMap_Region_Node)(nil), // 25: coder.tailnet.v2.DERPMap.Region.Node + nil, // 26: coder.tailnet.v2.Node.DerpLatencyEntry + nil, // 27: coder.tailnet.v2.Node.DerpForcedWebsocketEntry + (*CoordinateRequest_UpdateSelf)(nil), // 28: coder.tailnet.v2.CoordinateRequest.UpdateSelf + (*CoordinateRequest_Disconnect)(nil), // 29: coder.tailnet.v2.CoordinateRequest.Disconnect + (*CoordinateRequest_Tunnel)(nil), // 30: coder.tailnet.v2.CoordinateRequest.Tunnel + (*CoordinateRequest_ReadyForHandshake)(nil), // 31: coder.tailnet.v2.CoordinateRequest.ReadyForHandshake + (*CoordinateResponse_PeerUpdate)(nil), // 32: coder.tailnet.v2.CoordinateResponse.PeerUpdate + nil, // 33: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry + nil, // 34: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry + (*Netcheck_NetcheckIP)(nil), // 35: coder.tailnet.v2.Netcheck.NetcheckIP + (*TelemetryEvent_P2PEndpoint)(nil), // 36: coder.tailnet.v2.TelemetryEvent.P2PEndpoint + (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 38: google.protobuf.Duration + (*wrapperspb.BoolValue)(nil), // 39: google.protobuf.BoolValue + (*wrapperspb.FloatValue)(nil), // 40: google.protobuf.FloatValue } var file_tailnet_proto_tailnet_proto_depIdxs = []int32{ - 16, // 0: coder.tailnet.v2.DERPMap.home_params:type_name -> coder.tailnet.v2.DERPMap.HomeParams - 18, // 1: coder.tailnet.v2.DERPMap.regions:type_name -> coder.tailnet.v2.DERPMap.RegionsEntry - 32, // 2: coder.tailnet.v2.Node.as_of:type_name -> google.protobuf.Timestamp - 21, // 3: coder.tailnet.v2.Node.derp_latency:type_name -> coder.tailnet.v2.Node.DerpLatencyEntry - 22, // 4: coder.tailnet.v2.Node.derp_forced_websocket:type_name -> coder.tailnet.v2.Node.DerpForcedWebsocketEntry - 33, // 5: coder.tailnet.v2.RefreshResumeTokenResponse.refresh_in:type_name -> google.protobuf.Duration - 32, // 6: coder.tailnet.v2.RefreshResumeTokenResponse.expires_at:type_name -> google.protobuf.Timestamp - 23, // 7: coder.tailnet.v2.CoordinateRequest.update_self:type_name -> coder.tailnet.v2.CoordinateRequest.UpdateSelf - 24, // 8: coder.tailnet.v2.CoordinateRequest.disconnect:type_name -> coder.tailnet.v2.CoordinateRequest.Disconnect - 25, // 9: coder.tailnet.v2.CoordinateRequest.add_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel - 25, // 10: coder.tailnet.v2.CoordinateRequest.remove_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel - 26, // 11: coder.tailnet.v2.CoordinateRequest.ready_for_handshake:type_name -> coder.tailnet.v2.CoordinateRequest.ReadyForHandshake - 27, // 12: coder.tailnet.v2.CoordinateResponse.peer_updates:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate + 21, // 0: coder.tailnet.v2.DERPMap.home_params:type_name -> coder.tailnet.v2.DERPMap.HomeParams + 23, // 1: coder.tailnet.v2.DERPMap.regions:type_name -> coder.tailnet.v2.DERPMap.RegionsEntry + 37, // 2: coder.tailnet.v2.Node.as_of:type_name -> google.protobuf.Timestamp + 26, // 3: coder.tailnet.v2.Node.derp_latency:type_name -> coder.tailnet.v2.Node.DerpLatencyEntry + 27, // 4: coder.tailnet.v2.Node.derp_forced_websocket:type_name -> coder.tailnet.v2.Node.DerpForcedWebsocketEntry + 38, // 5: coder.tailnet.v2.RefreshResumeTokenResponse.refresh_in:type_name -> google.protobuf.Duration + 37, // 6: coder.tailnet.v2.RefreshResumeTokenResponse.expires_at:type_name -> google.protobuf.Timestamp + 28, // 7: coder.tailnet.v2.CoordinateRequest.update_self:type_name -> coder.tailnet.v2.CoordinateRequest.UpdateSelf + 29, // 8: coder.tailnet.v2.CoordinateRequest.disconnect:type_name -> coder.tailnet.v2.CoordinateRequest.Disconnect + 30, // 9: coder.tailnet.v2.CoordinateRequest.add_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel + 30, // 10: coder.tailnet.v2.CoordinateRequest.remove_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel + 31, // 11: coder.tailnet.v2.CoordinateRequest.ready_for_handshake:type_name -> coder.tailnet.v2.CoordinateRequest.ReadyForHandshake + 32, // 12: coder.tailnet.v2.CoordinateResponse.peer_updates:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate 1, // 13: coder.tailnet.v2.IPFields.class:type_name -> coder.tailnet.v2.IPFields.IPClass - 34, // 14: coder.tailnet.v2.Netcheck.OSHasIPv6:type_name -> google.protobuf.BoolValue - 34, // 15: coder.tailnet.v2.Netcheck.MappingVariesByDestIP:type_name -> google.protobuf.BoolValue - 34, // 16: coder.tailnet.v2.Netcheck.HairPinning:type_name -> google.protobuf.BoolValue - 34, // 17: coder.tailnet.v2.Netcheck.UPnP:type_name -> google.protobuf.BoolValue - 34, // 18: coder.tailnet.v2.Netcheck.PMP:type_name -> google.protobuf.BoolValue - 34, // 19: coder.tailnet.v2.Netcheck.PCP:type_name -> google.protobuf.BoolValue - 28, // 20: coder.tailnet.v2.Netcheck.RegionV4Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV4LatencyEntry - 29, // 21: coder.tailnet.v2.Netcheck.RegionV6Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV6LatencyEntry - 30, // 22: coder.tailnet.v2.Netcheck.GlobalV4:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP - 30, // 23: coder.tailnet.v2.Netcheck.GlobalV6:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP - 32, // 24: coder.tailnet.v2.TelemetryEvent.time:type_name -> google.protobuf.Timestamp + 39, // 14: coder.tailnet.v2.Netcheck.OSHasIPv6:type_name -> google.protobuf.BoolValue + 39, // 15: coder.tailnet.v2.Netcheck.MappingVariesByDestIP:type_name -> google.protobuf.BoolValue + 39, // 16: coder.tailnet.v2.Netcheck.HairPinning:type_name -> google.protobuf.BoolValue + 39, // 17: coder.tailnet.v2.Netcheck.UPnP:type_name -> google.protobuf.BoolValue + 39, // 18: coder.tailnet.v2.Netcheck.PMP:type_name -> google.protobuf.BoolValue + 39, // 19: coder.tailnet.v2.Netcheck.PCP:type_name -> google.protobuf.BoolValue + 33, // 20: coder.tailnet.v2.Netcheck.RegionV4Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV4LatencyEntry + 34, // 21: coder.tailnet.v2.Netcheck.RegionV6Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV6LatencyEntry + 35, // 22: coder.tailnet.v2.Netcheck.GlobalV4:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP + 35, // 23: coder.tailnet.v2.Netcheck.GlobalV6:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP + 37, // 24: coder.tailnet.v2.TelemetryEvent.time:type_name -> google.protobuf.Timestamp 2, // 25: coder.tailnet.v2.TelemetryEvent.status:type_name -> coder.tailnet.v2.TelemetryEvent.Status 3, // 26: coder.tailnet.v2.TelemetryEvent.client_type:type_name -> coder.tailnet.v2.TelemetryEvent.ClientType - 31, // 27: coder.tailnet.v2.TelemetryEvent.p2p_endpoint:type_name -> coder.tailnet.v2.TelemetryEvent.P2PEndpoint - 4, // 28: coder.tailnet.v2.TelemetryEvent.derp_map:type_name -> coder.tailnet.v2.DERPMap - 12, // 29: coder.tailnet.v2.TelemetryEvent.latest_netcheck:type_name -> coder.tailnet.v2.Netcheck - 33, // 30: coder.tailnet.v2.TelemetryEvent.connection_age:type_name -> google.protobuf.Duration - 33, // 31: coder.tailnet.v2.TelemetryEvent.connection_setup:type_name -> google.protobuf.Duration - 33, // 32: coder.tailnet.v2.TelemetryEvent.p2p_setup:type_name -> google.protobuf.Duration - 33, // 33: coder.tailnet.v2.TelemetryEvent.derp_latency:type_name -> google.protobuf.Duration - 33, // 34: coder.tailnet.v2.TelemetryEvent.p2p_latency:type_name -> google.protobuf.Duration - 35, // 35: coder.tailnet.v2.TelemetryEvent.throughput_mbits:type_name -> google.protobuf.FloatValue - 13, // 36: coder.tailnet.v2.TelemetryRequest.events:type_name -> coder.tailnet.v2.TelemetryEvent - 19, // 37: coder.tailnet.v2.DERPMap.HomeParams.region_score:type_name -> coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry - 20, // 38: coder.tailnet.v2.DERPMap.Region.nodes:type_name -> coder.tailnet.v2.DERPMap.Region.Node - 17, // 39: coder.tailnet.v2.DERPMap.RegionsEntry.value:type_name -> coder.tailnet.v2.DERPMap.Region - 6, // 40: coder.tailnet.v2.CoordinateRequest.UpdateSelf.node:type_name -> coder.tailnet.v2.Node - 6, // 41: coder.tailnet.v2.CoordinateResponse.PeerUpdate.node:type_name -> coder.tailnet.v2.Node - 0, // 42: coder.tailnet.v2.CoordinateResponse.PeerUpdate.kind:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind - 33, // 43: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry.value:type_name -> google.protobuf.Duration - 33, // 44: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry.value:type_name -> google.protobuf.Duration - 11, // 45: coder.tailnet.v2.Netcheck.NetcheckIP.fields:type_name -> coder.tailnet.v2.IPFields - 11, // 46: coder.tailnet.v2.TelemetryEvent.P2PEndpoint.fields:type_name -> coder.tailnet.v2.IPFields - 14, // 47: coder.tailnet.v2.Tailnet.PostTelemetry:input_type -> coder.tailnet.v2.TelemetryRequest - 5, // 48: coder.tailnet.v2.Tailnet.StreamDERPMaps:input_type -> coder.tailnet.v2.StreamDERPMapsRequest - 7, // 49: coder.tailnet.v2.Tailnet.RefreshResumeToken:input_type -> coder.tailnet.v2.RefreshResumeTokenRequest - 9, // 50: coder.tailnet.v2.Tailnet.Coordinate:input_type -> coder.tailnet.v2.CoordinateRequest - 15, // 51: coder.tailnet.v2.Tailnet.PostTelemetry:output_type -> coder.tailnet.v2.TelemetryResponse - 4, // 52: coder.tailnet.v2.Tailnet.StreamDERPMaps:output_type -> coder.tailnet.v2.DERPMap - 8, // 53: coder.tailnet.v2.Tailnet.RefreshResumeToken:output_type -> coder.tailnet.v2.RefreshResumeTokenResponse - 10, // 54: coder.tailnet.v2.Tailnet.Coordinate:output_type -> coder.tailnet.v2.CoordinateResponse - 51, // [51:55] is the sub-list for method output_type - 47, // [47:51] is the sub-list for method input_type - 47, // [47:47] is the sub-list for extension type_name - 47, // [47:47] is the sub-list for extension extendee - 0, // [0:47] is the sub-list for field type_name + 36, // 27: coder.tailnet.v2.TelemetryEvent.p2p_endpoint:type_name -> coder.tailnet.v2.TelemetryEvent.P2PEndpoint + 5, // 28: coder.tailnet.v2.TelemetryEvent.derp_map:type_name -> coder.tailnet.v2.DERPMap + 13, // 29: coder.tailnet.v2.TelemetryEvent.latest_netcheck:type_name -> coder.tailnet.v2.Netcheck + 38, // 30: coder.tailnet.v2.TelemetryEvent.connection_age:type_name -> google.protobuf.Duration + 38, // 31: coder.tailnet.v2.TelemetryEvent.connection_setup:type_name -> google.protobuf.Duration + 38, // 32: coder.tailnet.v2.TelemetryEvent.p2p_setup:type_name -> google.protobuf.Duration + 38, // 33: coder.tailnet.v2.TelemetryEvent.derp_latency:type_name -> google.protobuf.Duration + 38, // 34: coder.tailnet.v2.TelemetryEvent.p2p_latency:type_name -> google.protobuf.Duration + 40, // 35: coder.tailnet.v2.TelemetryEvent.throughput_mbits:type_name -> google.protobuf.FloatValue + 14, // 36: coder.tailnet.v2.TelemetryRequest.events:type_name -> coder.tailnet.v2.TelemetryEvent + 19, // 37: coder.tailnet.v2.WorkspaceUpdate.upserted_workspaces:type_name -> coder.tailnet.v2.Workspace + 20, // 38: coder.tailnet.v2.WorkspaceUpdate.upserted_agents:type_name -> coder.tailnet.v2.Agent + 19, // 39: coder.tailnet.v2.WorkspaceUpdate.deleted_workspaces:type_name -> coder.tailnet.v2.Workspace + 20, // 40: coder.tailnet.v2.WorkspaceUpdate.deleted_agents:type_name -> coder.tailnet.v2.Agent + 4, // 41: coder.tailnet.v2.Workspace.status:type_name -> coder.tailnet.v2.Workspace.Status + 24, // 42: coder.tailnet.v2.DERPMap.HomeParams.region_score:type_name -> coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry + 25, // 43: coder.tailnet.v2.DERPMap.Region.nodes:type_name -> coder.tailnet.v2.DERPMap.Region.Node + 22, // 44: coder.tailnet.v2.DERPMap.RegionsEntry.value:type_name -> coder.tailnet.v2.DERPMap.Region + 7, // 45: coder.tailnet.v2.CoordinateRequest.UpdateSelf.node:type_name -> coder.tailnet.v2.Node + 7, // 46: coder.tailnet.v2.CoordinateResponse.PeerUpdate.node:type_name -> coder.tailnet.v2.Node + 0, // 47: coder.tailnet.v2.CoordinateResponse.PeerUpdate.kind:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind + 38, // 48: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry.value:type_name -> google.protobuf.Duration + 38, // 49: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry.value:type_name -> google.protobuf.Duration + 12, // 50: coder.tailnet.v2.Netcheck.NetcheckIP.fields:type_name -> coder.tailnet.v2.IPFields + 12, // 51: coder.tailnet.v2.TelemetryEvent.P2PEndpoint.fields:type_name -> coder.tailnet.v2.IPFields + 15, // 52: coder.tailnet.v2.Tailnet.PostTelemetry:input_type -> coder.tailnet.v2.TelemetryRequest + 6, // 53: coder.tailnet.v2.Tailnet.StreamDERPMaps:input_type -> coder.tailnet.v2.StreamDERPMapsRequest + 8, // 54: coder.tailnet.v2.Tailnet.RefreshResumeToken:input_type -> coder.tailnet.v2.RefreshResumeTokenRequest + 10, // 55: coder.tailnet.v2.Tailnet.Coordinate:input_type -> coder.tailnet.v2.CoordinateRequest + 17, // 56: coder.tailnet.v2.Tailnet.WorkspaceUpdates:input_type -> coder.tailnet.v2.WorkspaceUpdatesRequest + 16, // 57: coder.tailnet.v2.Tailnet.PostTelemetry:output_type -> coder.tailnet.v2.TelemetryResponse + 5, // 58: coder.tailnet.v2.Tailnet.StreamDERPMaps:output_type -> coder.tailnet.v2.DERPMap + 9, // 59: coder.tailnet.v2.Tailnet.RefreshResumeToken:output_type -> coder.tailnet.v2.RefreshResumeTokenResponse + 11, // 60: coder.tailnet.v2.Tailnet.Coordinate:output_type -> coder.tailnet.v2.CoordinateResponse + 18, // 61: coder.tailnet.v2.Tailnet.WorkspaceUpdates:output_type -> coder.tailnet.v2.WorkspaceUpdate + 57, // [57:62] is the sub-list for method output_type + 52, // [52:57] is the sub-list for method input_type + 52, // [52:52] is the sub-list for extension type_name + 52, // [52:52] is the sub-list for extension extendee + 0, // [0:52] is the sub-list for field type_name } func init() { file_tailnet_proto_tailnet_proto_init() } @@ -2468,7 +2848,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DERPMap_HomeParams); i { + switch v := v.(*WorkspaceUpdatesRequest); i { case 0: return &v.state case 1: @@ -2480,7 +2860,31 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DERPMap_Region); i { + switch v := v.(*WorkspaceUpdate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Workspace); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Agent); i { case 0: return &v.state case 1: @@ -2492,6 +2896,30 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DERPMap_HomeParams); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DERPMap_Region); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DERPMap_Region_Node); i { case 0: return &v.state @@ -2503,7 +2931,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_UpdateSelf); i { case 0: return &v.state @@ -2515,7 +2943,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_Disconnect); i { case 0: return &v.state @@ -2527,7 +2955,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_Tunnel); i { case 0: return &v.state @@ -2539,7 +2967,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_ReadyForHandshake); i { case 0: return &v.state @@ -2551,7 +2979,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateResponse_PeerUpdate); i { case 0: return &v.state @@ -2563,7 +2991,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Netcheck_NetcheckIP); i { case 0: return &v.state @@ -2575,7 +3003,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TelemetryEvent_P2PEndpoint); i { case 0: return &v.state @@ -2593,8 +3021,8 @@ func file_tailnet_proto_tailnet_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_tailnet_proto_tailnet_proto_rawDesc, - NumEnums: 4, - NumMessages: 28, + NumEnums: 5, + NumMessages: 32, NumExtensions: 0, NumServices: 1, }, diff --git a/tailnet/proto/tailnet.proto b/tailnet/proto/tailnet.proto index b375ead7c7b63..55af05c08a375 100644 --- a/tailnet/proto/tailnet.proto +++ b/tailnet/proto/tailnet.proto @@ -198,9 +198,47 @@ message TelemetryRequest { message TelemetryResponse {} +message WorkspaceUpdatesRequest { + bytes workspace_owner_id = 1; // UUID +} + +message WorkspaceUpdate { + repeated Workspace upserted_workspaces = 1; + repeated Agent upserted_agents = 2; + repeated Workspace deleted_workspaces = 3; + repeated Agent deleted_agents = 4; +} + +message Workspace { + bytes id = 1; // UUID + string name = 2; + + enum Status { + UNKNOWN = 0; + PENDING = 1; + STARTING = 2; + RUNNING = 3; + STOPPING = 4; + STOPPED = 5; + FAILED = 6; + CANCELING = 7; + CANCELED = 8; + DELETING = 9; + DELETED = 10; + } + Status status = 3; +} + +message Agent { + bytes id = 1; // UUID + string name = 2; + bytes workspace_id = 3; // UUID +} + service Tailnet { rpc PostTelemetry(TelemetryRequest) returns (TelemetryResponse); rpc StreamDERPMaps(StreamDERPMapsRequest) returns (stream DERPMap); rpc RefreshResumeToken(RefreshResumeTokenRequest) returns (RefreshResumeTokenResponse); rpc Coordinate(stream CoordinateRequest) returns (stream CoordinateResponse); + rpc WorkspaceUpdates(WorkspaceUpdatesRequest) returns (stream WorkspaceUpdate); } diff --git a/tailnet/proto/tailnet_drpc.pb.go b/tailnet/proto/tailnet_drpc.pb.go index c0c3fcef65249..9dac4c06f3108 100644 --- a/tailnet/proto/tailnet_drpc.pb.go +++ b/tailnet/proto/tailnet_drpc.pb.go @@ -42,6 +42,7 @@ type DRPCTailnetClient interface { StreamDERPMaps(ctx context.Context, in *StreamDERPMapsRequest) (DRPCTailnet_StreamDERPMapsClient, error) RefreshResumeToken(ctx context.Context, in *RefreshResumeTokenRequest) (*RefreshResumeTokenResponse, error) Coordinate(ctx context.Context) (DRPCTailnet_CoordinateClient, error) + WorkspaceUpdates(ctx context.Context, in *WorkspaceUpdatesRequest) (DRPCTailnet_WorkspaceUpdatesClient, error) } type drpcTailnetClient struct { @@ -151,11 +152,52 @@ func (x *drpcTailnet_CoordinateClient) RecvMsg(m *CoordinateResponse) error { return x.MsgRecv(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}) } +func (c *drpcTailnetClient) WorkspaceUpdates(ctx context.Context, in *WorkspaceUpdatesRequest) (DRPCTailnet_WorkspaceUpdatesClient, error) { + stream, err := c.cc.NewStream(ctx, "/coder.tailnet.v2.Tailnet/WorkspaceUpdates", drpcEncoding_File_tailnet_proto_tailnet_proto{}) + if err != nil { + return nil, err + } + x := &drpcTailnet_WorkspaceUpdatesClient{stream} + if err := x.MsgSend(in, drpcEncoding_File_tailnet_proto_tailnet_proto{}); err != nil { + return nil, err + } + if err := x.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type DRPCTailnet_WorkspaceUpdatesClient interface { + drpc.Stream + Recv() (*WorkspaceUpdate, error) +} + +type drpcTailnet_WorkspaceUpdatesClient struct { + drpc.Stream +} + +func (x *drpcTailnet_WorkspaceUpdatesClient) GetStream() drpc.Stream { + return x.Stream +} + +func (x *drpcTailnet_WorkspaceUpdatesClient) Recv() (*WorkspaceUpdate, error) { + m := new(WorkspaceUpdate) + if err := x.MsgRecv(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcTailnet_WorkspaceUpdatesClient) RecvMsg(m *WorkspaceUpdate) error { + return x.MsgRecv(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}) +} + type DRPCTailnetServer interface { PostTelemetry(context.Context, *TelemetryRequest) (*TelemetryResponse, error) StreamDERPMaps(*StreamDERPMapsRequest, DRPCTailnet_StreamDERPMapsStream) error RefreshResumeToken(context.Context, *RefreshResumeTokenRequest) (*RefreshResumeTokenResponse, error) Coordinate(DRPCTailnet_CoordinateStream) error + WorkspaceUpdates(*WorkspaceUpdatesRequest, DRPCTailnet_WorkspaceUpdatesStream) error } type DRPCTailnetUnimplementedServer struct{} @@ -176,9 +218,13 @@ func (s *DRPCTailnetUnimplementedServer) Coordinate(DRPCTailnet_CoordinateStream return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCTailnetUnimplementedServer) WorkspaceUpdates(*WorkspaceUpdatesRequest, DRPCTailnet_WorkspaceUpdatesStream) error { + return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCTailnetDescription struct{} -func (DRPCTailnetDescription) NumMethods() int { return 4 } +func (DRPCTailnetDescription) NumMethods() int { return 5 } func (DRPCTailnetDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -217,6 +263,15 @@ func (DRPCTailnetDescription) Method(n int) (string, drpc.Encoding, drpc.Receive &drpcTailnet_CoordinateStream{in1.(drpc.Stream)}, ) }, DRPCTailnetServer.Coordinate, true + case 4: + return "/coder.tailnet.v2.Tailnet/WorkspaceUpdates", drpcEncoding_File_tailnet_proto_tailnet_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return nil, srv.(DRPCTailnetServer). + WorkspaceUpdates( + in1.(*WorkspaceUpdatesRequest), + &drpcTailnet_WorkspaceUpdatesStream{in2.(drpc.Stream)}, + ) + }, DRPCTailnetServer.WorkspaceUpdates, true default: return "", nil, nil, nil, false } @@ -296,3 +351,16 @@ func (x *drpcTailnet_CoordinateStream) Recv() (*CoordinateRequest, error) { func (x *drpcTailnet_CoordinateStream) RecvMsg(m *CoordinateRequest) error { return x.MsgRecv(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}) } + +type DRPCTailnet_WorkspaceUpdatesStream interface { + drpc.Stream + Send(*WorkspaceUpdate) error +} + +type drpcTailnet_WorkspaceUpdatesStream struct { + drpc.Stream +} + +func (x *drpcTailnet_WorkspaceUpdatesStream) Send(m *WorkspaceUpdate) error { + return x.MsgSend(m, drpcEncoding_File_tailnet_proto_tailnet_proto{}) +} diff --git a/tailnet/service.go b/tailnet/service.go index 7f38f63a589b3..cfbbb77a9833f 100644 --- a/tailnet/service.go +++ b/tailnet/service.go @@ -39,13 +39,28 @@ func WithStreamID(ctx context.Context, streamID StreamID) context.Context { return context.WithValue(ctx, streamIDContextKey{}, streamID) } +type WorkspaceUpdatesProvider interface { + io.Closer + Subscribe(ctx context.Context, userID uuid.UUID) (Subscription, error) +} + +type Subscription interface { + io.Closer + Updates() <-chan *proto.WorkspaceUpdate +} + +type TunnelAuthorizer interface { + AuthorizeTunnel(ctx context.Context, agentID uuid.UUID) error +} + type ClientServiceOptions struct { - Logger slog.Logger - CoordPtr *atomic.Pointer[Coordinator] - DERPMapUpdateFrequency time.Duration - DERPMapFn func() *tailcfg.DERPMap - NetworkTelemetryHandler func(batch []*proto.TelemetryEvent) - ResumeTokenProvider ResumeTokenProvider + Logger slog.Logger + CoordPtr *atomic.Pointer[Coordinator] + DERPMapUpdateFrequency time.Duration + DERPMapFn func() *tailcfg.DERPMap + NetworkTelemetryHandler func(batch []*proto.TelemetryEvent) + ResumeTokenProvider ResumeTokenProvider + WorkspaceUpdatesProvider WorkspaceUpdatesProvider } // ClientService is a tailnet coordination service that accepts a connection and version from a @@ -64,12 +79,13 @@ func NewClientService(options ClientServiceOptions) ( s := &ClientService{Logger: options.Logger, CoordPtr: options.CoordPtr} mux := drpcmux.New() drpcService := &DRPCService{ - CoordPtr: options.CoordPtr, - Logger: options.Logger, - DerpMapUpdateFrequency: options.DERPMapUpdateFrequency, - DerpMapFn: options.DERPMapFn, - NetworkTelemetryHandler: options.NetworkTelemetryHandler, - ResumeTokenProvider: options.ResumeTokenProvider, + CoordPtr: options.CoordPtr, + Logger: options.Logger, + DerpMapUpdateFrequency: options.DERPMapUpdateFrequency, + DerpMapFn: options.DERPMapFn, + NetworkTelemetryHandler: options.NetworkTelemetryHandler, + ResumeTokenProvider: options.ResumeTokenProvider, + WorkspaceUpdatesProvider: options.WorkspaceUpdatesProvider, } err := proto.DRPCRegisterTailnet(mux, drpcService) if err != nil { @@ -89,7 +105,7 @@ func NewClientService(options ClientServiceOptions) ( return s, nil } -func (s *ClientService) ServeClient(ctx context.Context, version string, conn net.Conn, id uuid.UUID, agent uuid.UUID) error { +func (s *ClientService) ServeClient(ctx context.Context, version string, conn net.Conn, streamID StreamID) error { major, _, err := apiversion.Parse(version) if err != nil { s.Logger.Warn(ctx, "serve client called with unparsable version", slog.Error(err)) @@ -97,12 +113,6 @@ func (s *ClientService) ServeClient(ctx context.Context, version string, conn ne } switch major { case 2: - auth := ClientCoordinateeAuth{AgentID: agent} - streamID := StreamID{ - Name: "client", - ID: id, - Auth: auth, - } return s.ServeConnV2(ctx, conn, streamID) default: s.Logger.Warn(ctx, "serve client called with unsupported version", slog.F("version", version)) @@ -125,12 +135,13 @@ func (s ClientService) ServeConnV2(ctx context.Context, conn net.Conn, streamID // DRPCService is the dRPC-based, version 2.x of the tailnet API and implements proto.DRPCClientServer type DRPCService struct { - CoordPtr *atomic.Pointer[Coordinator] - Logger slog.Logger - DerpMapUpdateFrequency time.Duration - DerpMapFn func() *tailcfg.DERPMap - NetworkTelemetryHandler func(batch []*proto.TelemetryEvent) - ResumeTokenProvider ResumeTokenProvider + CoordPtr *atomic.Pointer[Coordinator] + Logger slog.Logger + DerpMapUpdateFrequency time.Duration + DerpMapFn func() *tailcfg.DERPMap + NetworkTelemetryHandler func(batch []*proto.TelemetryEvent) + ResumeTokenProvider ResumeTokenProvider + WorkspaceUpdatesProvider WorkspaceUpdatesProvider } func (s *DRPCService) PostTelemetry(_ context.Context, req *proto.TelemetryRequest) (*proto.TelemetryResponse, error) { @@ -205,6 +216,38 @@ func (s *DRPCService) Coordinate(stream proto.DRPCTailnet_CoordinateStream) erro return nil } +func (s *DRPCService) WorkspaceUpdates(req *proto.WorkspaceUpdatesRequest, stream proto.DRPCTailnet_WorkspaceUpdatesStream) error { + defer stream.Close() + + ctx := stream.Context() + + ownerID, err := uuid.FromBytes(req.WorkspaceOwnerId) + if err != nil { + return xerrors.Errorf("parse workspace owner ID: %w", err) + } + + sub, err := s.WorkspaceUpdatesProvider.Subscribe(ctx, ownerID) + if err != nil { + return xerrors.Errorf("subscribe to workspace updates: %w", err) + } + defer sub.Close() + + for { + select { + case updates, ok := <-sub.Updates(): + if !ok { + return nil + } + err := stream.Send(updates) + if err != nil { + return xerrors.Errorf("send workspace update: %w", err) + } + case <-stream.Context().Done(): + return nil + } + } +} + type communicator struct { logger slog.Logger stream proto.DRPCTailnet_CoordinateStream diff --git a/tailnet/service_test.go b/tailnet/service_test.go index 0f4b4795c42e9..f5a01cc2fbacc 100644 --- a/tailnet/service_test.go +++ b/tailnet/service_test.go @@ -1,6 +1,7 @@ package tailnet_test import ( + "context" "io" "net" "sync/atomic" @@ -52,7 +53,13 @@ func TestClientService_ServeClient_V2(t *testing.T) { agentID := uuid.MustParse("20000001-0000-0000-0000-000000000000") errCh := make(chan error, 1) go func() { - err := uut.ServeClient(ctx, "2.0", s, clientID, agentID) + err := uut.ServeClient(ctx, "2.0", s, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: agentID, + }, + }) t.Logf("ServeClient returned; err=%v", err) errCh <- err }() @@ -74,7 +81,7 @@ func TestClientService_ServeClient_V2(t *testing.T) { require.NotNil(t, call) require.Equal(t, call.ID, clientID) require.Equal(t, call.Name, "client") - require.NoError(t, call.Auth.Authorize(&proto.CoordinateRequest{ + require.NoError(t, call.Auth.Authorize(ctx, &proto.CoordinateRequest{ AddTunnel: &proto.CoordinateRequest_Tunnel{Id: agentID[:]}, })) req := testutil.RequireRecvCtx(ctx, t, call.Reqs) @@ -157,7 +164,13 @@ func TestClientService_ServeClient_V1(t *testing.T) { agentID := uuid.MustParse("20000001-0000-0000-0000-000000000000") errCh := make(chan error, 1) go func() { - err := uut.ServeClient(ctx, "1.0", s, clientID, agentID) + err := uut.ServeClient(ctx, "1.0", s, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: agentID, + }, + }) t.Logf("ServeClient returned; err=%v", err) errCh <- err }() @@ -213,3 +226,170 @@ func TestNetworkTelemetryBatcher(t *testing.T) { require.Equal(t, "5", string(batch[0].Id)) require.Equal(t, "6", string(batch[1].Id)) } + +func TestClientUserCoordinateeAuth(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + agentID := uuid.UUID{0x01} + agentID2 := uuid.UUID{0x02} + clientID := uuid.UUID{0x03} + + updatesCh := make(chan *proto.WorkspaceUpdate, 1) + updatesProvider := &fakeUpdatesProvider{ch: updatesCh} + + fCoord, client := createUpdateService(t, ctx, clientID, updatesProvider) + + // Coordinate + stream, err := client.Coordinate(ctx) + require.NoError(t, err) + defer stream.Close() + + err = stream.Send(&proto.CoordinateRequest{ + UpdateSelf: &proto.CoordinateRequest_UpdateSelf{Node: &proto.Node{PreferredDerp: 11}}, + }) + require.NoError(t, err) + + call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) + require.NotNil(t, call) + require.Equal(t, call.ID, clientID) + require.Equal(t, call.Name, "client") + req := testutil.RequireRecvCtx(ctx, t, call.Reqs) + require.Equal(t, int32(11), req.GetUpdateSelf().GetNode().GetPreferredDerp()) + + // Authorize uses `ClientUserCoordinateeAuth` + require.NoError(t, call.Auth.Authorize(ctx, &proto.CoordinateRequest{ + AddTunnel: &proto.CoordinateRequest_Tunnel{Id: tailnet.UUIDToByteSlice(agentID)}, + })) + require.Error(t, call.Auth.Authorize(ctx, &proto.CoordinateRequest{ + AddTunnel: &proto.CoordinateRequest_Tunnel{Id: tailnet.UUIDToByteSlice(agentID2)}, + })) +} + +func TestWorkspaceUpdates(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + updatesCh := make(chan *proto.WorkspaceUpdate, 1) + updatesProvider := &fakeUpdatesProvider{ch: updatesCh} + + clientID := uuid.UUID{0x03} + wsID := uuid.UUID{0x04} + + _, client := createUpdateService(t, ctx, clientID, updatesProvider) + + // Workspace updates + expected := &proto.WorkspaceUpdate{ + UpsertedWorkspaces: []*proto.Workspace{ + { + Id: tailnet.UUIDToByteSlice(wsID), + Name: "ws1", + Status: proto.Workspace_RUNNING, + }, + }, + UpsertedAgents: []*proto.Agent{}, + DeletedWorkspaces: []*proto.Workspace{}, + DeletedAgents: []*proto.Agent{}, + } + updatesCh <- expected + + updatesStream, err := client.WorkspaceUpdates(ctx, &proto.WorkspaceUpdatesRequest{ + WorkspaceOwnerId: tailnet.UUIDToByteSlice(clientID), + }) + require.NoError(t, err) + defer updatesStream.Close() + + updates, err := updatesStream.Recv() + require.NoError(t, err) + require.Len(t, updates.GetUpsertedWorkspaces(), 1) + require.Equal(t, expected.GetUpsertedWorkspaces()[0].GetName(), updates.GetUpsertedWorkspaces()[0].GetName()) + require.Equal(t, expected.GetUpsertedWorkspaces()[0].GetStatus(), updates.GetUpsertedWorkspaces()[0].GetStatus()) + require.Equal(t, expected.GetUpsertedWorkspaces()[0].GetId(), updates.GetUpsertedWorkspaces()[0].GetId()) +} + +//nolint:revive // t takes precedence +func createUpdateService(t *testing.T, ctx context.Context, clientID uuid.UUID, updates tailnet.WorkspaceUpdatesProvider) (*tailnettest.FakeCoordinator, proto.DRPCTailnetClient) { + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + uut, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger, + CoordPtr: &coordPtr, + WorkspaceUpdatesProvider: updates, + }) + require.NoError(t, err) + + c, s := net.Pipe() + t.Cleanup(func() { + _ = c.Close() + _ = s.Close() + }) + + errCh := make(chan error, 1) + go func() { + err := uut.ServeClient(ctx, "2.0", s, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientUserCoordinateeAuth{ + Auth: &fakeTunnelAuth{}, + }, + }) + t.Logf("ServeClient returned; err=%v", err) + errCh <- err + }() + + client, err := tailnet.NewDRPCClient(c, logger) + require.NoError(t, err) + + t.Cleanup(func() { + err = c.Close() + require.NoError(t, err) + err = testutil.RequireRecvCtx(ctx, t, errCh) + require.True(t, xerrors.Is(err, io.EOF) || xerrors.Is(err, io.ErrClosedPipe)) + }) + return fCoord, client +} + +type fakeUpdatesProvider struct { + ch chan *proto.WorkspaceUpdate +} + +func (*fakeUpdatesProvider) Close() error { + return nil +} + +func (f *fakeUpdatesProvider) Subscribe(context.Context, uuid.UUID) (tailnet.Subscription, error) { + return &fakeSubscription{ch: f.ch}, nil +} + +type fakeSubscription struct { + ch chan *proto.WorkspaceUpdate +} + +func (*fakeSubscription) Close() error { + return nil +} + +func (f *fakeSubscription) Updates() <-chan *proto.WorkspaceUpdate { + return f.ch +} + +var _ tailnet.Subscription = (*fakeSubscription)(nil) + +var _ tailnet.WorkspaceUpdatesProvider = (*fakeUpdatesProvider)(nil) + +type fakeTunnelAuth struct{} + +// AuthorizeTunnel implements tailnet.TunnelAuthorizer. +func (*fakeTunnelAuth) AuthorizeTunnel(_ context.Context, agentID uuid.UUID) error { + if agentID[0] != 1 { + return xerrors.New("policy disallows request") + } + return nil +} + +var _ tailnet.TunnelAuthorizer = (*fakeTunnelAuth)(nil) diff --git a/tailnet/test/peer.go b/tailnet/test/peer.go index ce9a50749901f..9426beac860b7 100644 --- a/tailnet/test/peer.go +++ b/tailnet/test/peer.go @@ -370,3 +370,20 @@ func (p *Peer) UngracefulDisconnect(ctx context.Context) { close(p.reqs) p.Close(ctx) } + +type FakeSubjectKey struct{} + +type FakeCoordinateeAuth struct { + Chan chan struct{} +} + +func (f FakeCoordinateeAuth) Authorize(ctx context.Context, _ *proto.CoordinateRequest) error { + _, ok := ctx.Value(FakeSubjectKey{}).(struct{}) + if !ok { + return xerrors.New("unauthorized") + } + f.Chan <- struct{}{} + return nil +} + +var _ tailnet.CoordinateeAuth = (*FakeCoordinateeAuth)(nil) diff --git a/tailnet/tunnel.go b/tailnet/tunnel.go index 3e55abb955513..c1335f4c17d01 100644 --- a/tailnet/tunnel.go +++ b/tailnet/tunnel.go @@ -1,6 +1,7 @@ package tailnet import ( + "context" "net/netip" "github.com/google/uuid" @@ -12,13 +13,13 @@ import ( var legacyWorkspaceAgentIP = netip.MustParseAddr("fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4") type CoordinateeAuth interface { - Authorize(req *proto.CoordinateRequest) error + Authorize(ctx context.Context, req *proto.CoordinateRequest) error } // SingleTailnetCoordinateeAuth allows all tunnels, since Coderd and wsproxy are allowed to initiate a tunnel to any agent type SingleTailnetCoordinateeAuth struct{} -func (SingleTailnetCoordinateeAuth) Authorize(*proto.CoordinateRequest) error { +func (SingleTailnetCoordinateeAuth) Authorize(context.Context, *proto.CoordinateRequest) error { return nil } @@ -27,7 +28,7 @@ type ClientCoordinateeAuth struct { AgentID uuid.UUID } -func (c ClientCoordinateeAuth) Authorize(req *proto.CoordinateRequest) error { +func (c ClientCoordinateeAuth) Authorize(_ context.Context, req *proto.CoordinateRequest) error { if tun := req.GetAddTunnel(); tun != nil { uid, err := uuid.FromBytes(tun.Id) if err != nil { @@ -39,6 +40,19 @@ func (c ClientCoordinateeAuth) Authorize(req *proto.CoordinateRequest) error { } } + return handleClientNodeRequests(req) +} + +// AgentCoordinateeAuth disallows all tunnels, since agents are not allowed to initiate their own tunnels +type AgentCoordinateeAuth struct { + ID uuid.UUID +} + +func (a AgentCoordinateeAuth) Authorize(_ context.Context, req *proto.CoordinateRequest) error { + if tun := req.GetAddTunnel(); tun != nil { + return xerrors.New("agents cannot open tunnels") + } + if upd := req.GetUpdateSelf(); upd != nil { for _, addrStr := range upd.Node.Addresses { pre, err := netip.ParsePrefix(addrStr) @@ -49,26 +63,39 @@ func (c ClientCoordinateeAuth) Authorize(req *proto.CoordinateRequest) error { if pre.Bits() != 128 { return xerrors.Errorf("invalid address bits, expected 128, got %d", pre.Bits()) } - } - } - if rfh := req.GetReadyForHandshake(); rfh != nil { - return xerrors.Errorf("clients may not send ready_for_handshake") + if TailscaleServicePrefix.AddrFromUUID(a.ID).Compare(pre.Addr()) != 0 && + CoderServicePrefix.AddrFromUUID(a.ID).Compare(pre.Addr()) != 0 && + legacyWorkspaceAgentIP.Compare(pre.Addr()) != 0 { + return xerrors.Errorf("invalid node address, got %s", pre.Addr().String()) + } + } } return nil } -// AgentCoordinateeAuth disallows all tunnels, since agents are not allowed to initiate their own tunnels -type AgentCoordinateeAuth struct { - ID uuid.UUID +type ClientUserCoordinateeAuth struct { + Auth TunnelAuthorizer } -func (a AgentCoordinateeAuth) Authorize(req *proto.CoordinateRequest) error { +func (a ClientUserCoordinateeAuth) Authorize(ctx context.Context, req *proto.CoordinateRequest) error { if tun := req.GetAddTunnel(); tun != nil { - return xerrors.New("agents cannot open tunnels") + uid, err := uuid.FromBytes(tun.Id) + if err != nil { + return xerrors.Errorf("parse add tunnel id: %w", err) + } + err = a.Auth.AuthorizeTunnel(ctx, uid) + if err != nil { + return xerrors.Errorf("workspace agent not found or you do not have permission") + } } + return handleClientNodeRequests(req) +} + +// handleClientNodeRequests validates GetUpdateSelf requests and declines ReadyForHandshake requests +func handleClientNodeRequests(req *proto.CoordinateRequest) error { if upd := req.GetUpdateSelf(); upd != nil { for _, addrStr := range upd.Node.Addresses { pre, err := netip.ParsePrefix(addrStr) @@ -79,15 +106,12 @@ func (a AgentCoordinateeAuth) Authorize(req *proto.CoordinateRequest) error { if pre.Bits() != 128 { return xerrors.Errorf("invalid address bits, expected 128, got %d", pre.Bits()) } - - if TailscaleServicePrefix.AddrFromUUID(a.ID).Compare(pre.Addr()) != 0 && - CoderServicePrefix.AddrFromUUID(a.ID).Compare(pre.Addr()) != 0 && - legacyWorkspaceAgentIP.Compare(pre.Addr()) != 0 { - return xerrors.Errorf("invalid node address, got %s", pre.Addr().String()) - } } } + if rfh := req.GetReadyForHandshake(); rfh != nil { + return xerrors.Errorf("clients may not send ready_for_handshake") + } return nil } From bd9151d2248702773ec2aee4559a5d59b5dd602d Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 1 Nov 2024 09:24:29 +0200 Subject: [PATCH 021/223] fix(coderd/database/pubsub): prevent listeners read outside mutex lock (#15303) https://github.com/coder/coder/actions/runs/11611105362/job/32331771969#logs ``` 2024-10-31T11:36:45.9225038Z WARNING: DATA RACE 2024-10-31T11:36:45.9225120Z Write at 0x00c0000d8030 by goroutine 26: 2024-10-31T11:36:45.9225200Z runtime.mapdelete() 2024-10-31T11:36:45.9225412Z /opt/hostedtoolcache/go/1.22.8/x64/src/runtime/map.go:696 +0x0 2024-10-31T11:36:45.9225647Z github.com/coder/coder/v2/coderd/database/pubsub.(*PGPubsub).subscribeQueue.func2() 2024-10-31T11:36:45.9225906Z /home/runner/work/coder/coder/coderd/database/pubsub/pubsub.go:277 +0x131 2024-10-31T11:36:45.9225993Z runtime.deferreturn() 2024-10-31T11:36:45.9226210Z /opt/hostedtoolcache/go/1.22.8/x64/src/runtime/panic.go:602 +0x5d 2024-10-31T11:36:45.9226283Z testing.tRunner() 2024-10-31T11:36:45.9226519Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1689 +0x21e 2024-10-31T11:36:45.9226603Z testing.(*T).Run.gowrap1() 2024-10-31T11:36:45.9226831Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1742 +0x44 2024-10-31T11:36:45.9226836Z 2024-10-31T11:36:45.9226934Z Previous read at 0x00c0000d8030 by goroutine 112: 2024-10-31T11:36:45.9227159Z github.com/coder/coder/v2/coderd/database/pubsub.(*PGPubsub).subscribeQueue.func2() 2024-10-31T11:36:45.9227462Z /home/runner/work/coder/coder/coderd/database/pubsub/pubsub.go:284 +0x1b6 2024-10-31T11:36:45.9227661Z github.com/coder/coder/v2/enterprise/replicasync.(*Manager).subscribe.func3() 2024-10-31T11:36:45.9227936Z /home/runner/work/coder/coder/enterprise/replicasync/replicasync.go:228 +0x53 2024-10-31T11:36:45.9227941Z 2024-10-31T11:36:45.9228019Z Goroutine 26 (running) created at: 2024-10-31T11:36:45.9228096Z testing.(*T).Run() 2024-10-31T11:36:45.9228318Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1742 +0x825 2024-10-31T11:36:45.9228498Z github.com/coder/coder/v2/enterprise/replicasync_test.TestReplica() 2024-10-31T11:36:45.9228777Z /home/runner/work/coder/coder/enterprise/replicasync/replicasync_test.go:33 +0x4b 2024-10-31T11:36:45.9228847Z testing.tRunner() 2024-10-31T11:36:45.9229063Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1689 +0x21e 2024-10-31T11:36:45.9229142Z testing.(*T).Run.gowrap1() 2024-10-31T11:36:45.9229366Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1742 +0x44 2024-10-31T11:36:45.9229369Z 2024-10-31T11:36:45.9229443Z Goroutine 112 (finished) created at: 2024-10-31T11:36:45.9229685Z github.com/coder/coder/v2/enterprise/replicasync.(*Manager).subscribe() 2024-10-31T11:36:45.9229952Z /home/runner/work/coder/coder/enterprise/replicasync/replicasync.go:226 +0x568 2024-10-31T11:36:45.9230092Z github.com/coder/coder/v2/enterprise/replicasync.New() 2024-10-31T11:36:45.9230361Z /home/runner/work/coder/coder/enterprise/replicasync/replicasync.go:101 +0x1344 2024-10-31T11:36:45.9230547Z github.com/coder/coder/v2/enterprise/replicasync_test.TestReplica.func1() 2024-10-31T11:36:45.9230836Z /home/runner/work/coder/coder/enterprise/replicasync/replicasync_test.go:48 +0x26a 2024-10-31T11:36:45.9230904Z testing.tRunner() 2024-10-31T11:36:45.9231127Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1689 +0x21e 2024-10-31T11:36:45.9231207Z testing.(*T).Run.gowrap1() 2024-10-31T11:36:45.9231431Z /opt/hostedtoolcache/go/1.22.8/x64/src/testing/testing.go:1742 +0x44 ``` --- coderd/database/pubsub/pubsub.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/database/pubsub/pubsub.go b/coderd/database/pubsub/pubsub.go index fa4dc8b90b1d0..c0130e3deac04 100644 --- a/coderd/database/pubsub/pubsub.go +++ b/coderd/database/pubsub/pubsub.go @@ -278,10 +278,11 @@ func (p *PGPubsub) subscribeQueue(event string, newQ *msgQueue) (cancel func(), if len(listeners) == 0 { delete(p.queues, event) } + listenerCount := len(listeners) p.qMu.Unlock() // as above, we must not hold the lock while calling into pgListener - if len(listeners) == 0 { + if listenerCount == 0 { uErr := p.pgListener.Unlisten(event) p.closeMu.Lock() defer p.closeMu.Unlock() From fbbefa228d5f725877dfd9af55062d45f8f1fa0e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 1 Nov 2024 09:58:52 +0000 Subject: [PATCH 022/223] chore(.gitignore): add .zed_server to .gitignore (#15316) --- .gitignore | 3 +++ .prettierignore | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 29081a803f217..0c07d1eea52f5 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ result # pnpm .pnpm-store/ + +# Zed +.zed_server diff --git a/.prettierignore b/.prettierignore index 87b917aa43113..0391d7ede4e7a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -74,6 +74,9 @@ result # pnpm .pnpm-store/ + +# Zed +.zed_server # .prettierignore.include: # Helm templates contain variables that are invalid YAML and can't be formatted # by Prettier. From 005ea536a5094794855b0196a3b7111dda631db8 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 1 Nov 2024 14:35:26 +0400 Subject: [PATCH 023/223] fix: fix Listen/Unlisten race on Pubsub (#15315) Fixes #15312 When we need to `Unlisten()` for an event, instead of immediately removing the event from the `p.queues`, we store a channel to signal any goroutines trying to Subscribe to the same event when we are done. On `Subscribe`, if the channel is present, wait for it before calling `Listen` to ensure the ordering is correct. --- coderd/database/pubsub/pubsub.go | 124 +++++++++++++----- .../database/pubsub/pubsub_internal_test.go | 62 +++++++++ 2 files changed, 155 insertions(+), 31 deletions(-) diff --git a/coderd/database/pubsub/pubsub.go b/coderd/database/pubsub/pubsub.go index c0130e3deac04..6823dc0188ef3 100644 --- a/coderd/database/pubsub/pubsub.go +++ b/coderd/database/pubsub/pubsub.go @@ -11,7 +11,6 @@ import ( "sync/atomic" "time" - "github.com/google/uuid" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" @@ -188,6 +187,19 @@ func (l pqListenerShim) NotifyChan() <-chan *pq.Notification { return l.Notify } +type queueSet struct { + m map[*msgQueue]struct{} + // unlistenInProgress will be non-nil if another goroutine is unlistening for the event this + // queueSet corresponds to. If non-nil, that goroutine will close the channel when it is done. + unlistenInProgress chan struct{} +} + +func newQueueSet() *queueSet { + return &queueSet{ + m: make(map[*msgQueue]struct{}), + } +} + // PGPubsub is a pubsub implementation using PostgreSQL. type PGPubsub struct { logger slog.Logger @@ -196,7 +208,7 @@ type PGPubsub struct { db *sql.DB qMu sync.Mutex - queues map[string]map[uuid.UUID]*msgQueue + queues map[string]*queueSet // making the close state its own mutex domain simplifies closing logic so // that we don't have to hold the qMu --- which could block processing @@ -243,6 +255,48 @@ func (p *PGPubsub) subscribeQueue(event string, newQ *msgQueue) (cancel func(), } }() + var ( + unlistenInProgress <-chan struct{} + // MUST hold the p.qMu lock to manipulate this! + qs *queueSet + ) + func() { + p.qMu.Lock() + defer p.qMu.Unlock() + + var ok bool + if qs, ok = p.queues[event]; !ok { + qs = newQueueSet() + p.queues[event] = qs + } + qs.m[newQ] = struct{}{} + unlistenInProgress = qs.unlistenInProgress + }() + // NOTE there cannot be any `return` statements between here and the next +-+, otherwise the + // assumptions the defer makes could be violated + if unlistenInProgress != nil { + // We have to wait here because we don't want our `Listen` call to happen before the other + // goroutine calls `Unlisten`. That would result in this subscription not getting any + // events. c.f. https://github.com/coder/coder/issues/15312 + p.logger.Debug(context.Background(), "waiting for Unlisten in progress", slog.F("event", event)) + <-unlistenInProgress + p.logger.Debug(context.Background(), "unlistening complete", slog.F("event", event)) + } + // +-+ (see above) + defer func() { + if err != nil { + p.qMu.Lock() + defer p.qMu.Unlock() + delete(qs.m, newQ) + if len(qs.m) == 0 { + // we know that newQ was in the queueSet since we last unlocked, so there cannot + // have been any _new_ goroutines trying to Unlisten(). Therefore, if the queueSet + // is now empty, it's safe to delete. + delete(p.queues, event) + } + } + }() + // The pgListener waits for the response to `LISTEN` on a mainloop that also dispatches // notifies. We need to avoid holding the mutex while this happens, since holding the mutex // blocks reading notifications and can deadlock the pgListener. @@ -258,32 +312,40 @@ func (p *PGPubsub) subscribeQueue(event string, newQ *msgQueue) (cancel func(), if err != nil { return nil, xerrors.Errorf("listen: %w", err) } - p.qMu.Lock() - defer p.qMu.Unlock() - var eventQs map[uuid.UUID]*msgQueue - var ok bool - if eventQs, ok = p.queues[event]; !ok { - eventQs = make(map[uuid.UUID]*msgQueue) - p.queues[event] = eventQs - } - id := uuid.New() - eventQs[id] = newQ return func() { - p.qMu.Lock() - listeners := p.queues[event] - q := listeners[id] - q.close() - delete(listeners, id) - if len(listeners) == 0 { - delete(p.queues, event) - } - listenerCount := len(listeners) - p.qMu.Unlock() - // as above, we must not hold the lock while calling into pgListener + var unlistening chan struct{} + func() { + p.qMu.Lock() + defer p.qMu.Unlock() + newQ.close() + qSet, ok := p.queues[event] + if !ok { + p.logger.Critical(context.Background(), "event was removed before cancel", slog.F("event", event)) + return + } + delete(qSet.m, newQ) + if len(qSet.m) == 0 { + unlistening = make(chan struct{}) + qSet.unlistenInProgress = unlistening + } + }() - if listenerCount == 0 { + // as above, we must not hold the lock while calling into pgListener + if unlistening != nil { uErr := p.pgListener.Unlisten(event) + close(unlistening) + // we can now delete the queueSet if it is empty. + func() { + p.qMu.Lock() + defer p.qMu.Unlock() + qSet, ok := p.queues[event] + if ok && len(qSet.m) == 0 { + p.logger.Debug(context.Background(), "removing queueSet", slog.F("event", event)) + delete(p.queues, event) + } + }() + p.closeMu.Lock() defer p.closeMu.Unlock() if uErr != nil && !p.closedListener { @@ -361,12 +423,12 @@ func (p *PGPubsub) listenReceive(notif *pq.Notification) { p.qMu.Lock() defer p.qMu.Unlock() - queues, ok := p.queues[notif.Channel] + qSet, ok := p.queues[notif.Channel] if !ok { return } extra := []byte(notif.Extra) - for _, q := range queues { + for q := range qSet.m { q.enqueue(extra) } } @@ -374,8 +436,8 @@ func (p *PGPubsub) listenReceive(notif *pq.Notification) { func (p *PGPubsub) recordReconnect() { p.qMu.Lock() defer p.qMu.Unlock() - for _, listeners := range p.queues { - for _, q := range listeners { + for _, qSet := range p.queues { + for q := range qSet.m { q.dropped() } } @@ -590,8 +652,8 @@ func (p *PGPubsub) Collect(metrics chan<- prometheus.Metric) { p.qMu.Lock() events := len(p.queues) subs := 0 - for _, subscriberMap := range p.queues { - subs += len(subscriberMap) + for _, qSet := range p.queues { + subs += len(qSet.m) } p.qMu.Unlock() metrics <- prometheus.MustNewConstMetric(currentSubscribersDesc, prometheus.GaugeValue, float64(subs)) @@ -629,7 +691,7 @@ func newWithoutListener(logger slog.Logger, db *sql.DB) *PGPubsub { logger: logger, listenDone: make(chan struct{}), db: db, - queues: make(map[string]map[uuid.UUID]*msgQueue), + queues: make(map[string]*queueSet), latencyMeasurer: NewLatencyMeasurer(logger.Named("latency-measurer")), publishesTotal: prometheus.NewCounterVec(prometheus.CounterOpts{ diff --git a/coderd/database/pubsub/pubsub_internal_test.go b/coderd/database/pubsub/pubsub_internal_test.go index 2587357153ee8..df54ca5498f34 100644 --- a/coderd/database/pubsub/pubsub_internal_test.go +++ b/coderd/database/pubsub/pubsub_internal_test.go @@ -178,6 +178,60 @@ func TestPubSub_DoesntBlockNotify(t *testing.T) { require.NoError(t, err) } +// TestPubSub_DoesntRaceListenUnlisten tests for regressions of +// https://github.com/coder/coder/issues/15312 +func TestPubSub_DoesntRaceListenUnlisten(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + uut := newWithoutListener(logger, nil) + fListener := newFakePqListener() + uut.pgListener = fListener + go uut.listen() + + noopListener := func(_ context.Context, _ []byte) {} + + const numEvents = 500 + events := make([]string, numEvents) + cancels := make([]func(), numEvents) + for i := range events { + var err error + events[i] = fmt.Sprintf("event-%d", i) + cancels[i], err = uut.Subscribe(events[i], noopListener) + require.NoError(t, err) + } + start := make(chan struct{}) + done := make(chan struct{}) + finalCancels := make([]func(), numEvents) + for i := range events { + event := events[i] + cancel := cancels[i] + go func() { + <-start + var err error + // subscribe again + finalCancels[i], err = uut.Subscribe(event, noopListener) + assert.NoError(t, err) + done <- struct{}{} + }() + go func() { + <-start + cancel() + done <- struct{}{} + }() + } + close(start) + for range numEvents * 2 { + _ = testutil.RequireRecvCtx(ctx, t, done) + } + for i := range events { + fListener.requireIsListening(t, events[i]) + finalCancels[i]() + } + require.Len(t, uut.queues, 0) +} + const ( numNotifications = 5 testMessage = "birds of a feather" @@ -255,3 +309,11 @@ func newFakePqListener() *fakePqListener { notify: make(chan *pq.Notification), } } + +func (f *fakePqListener) requireIsListening(t testing.TB, s string) { + t.Helper() + f.mu.Lock() + defer f.mu.Unlock() + _, ok := f.channels[s] + require.True(t, ok, "should be listening for '%s', but isn't", s) +} From 5f60a8d9e35b8f4dea940e082a8ec347cdd27a57 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:22:36 +1100 Subject: [PATCH 024/223] fix: create contexts per sub-test to fix flake (#15314) Flake seen here: https://github.com/coder/coder/actions/runs/11624011543/job/32371950701 https://coder.com/blog/go-testing-contexts-and-t-parallel --- coderd/database/querier_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 41fca8d0a453e..b3fde5a558e6b 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -620,7 +620,6 @@ func TestGetAuthorizedWorkspacesAndAgentsByOwnerID(t *testing.T) { t.SkipNow() } - ctx := testutil.Context(t, testutil.WaitLong) sqlDB := testSQLDB(t) err := migrations.Up(sqlDB) require.NoError(t, err) @@ -695,6 +694,7 @@ func TestGetAuthorizedWorkspacesAndAgentsByOwnerID(t *testing.T) { } t.Run("sqlQuerier", func(t *testing.T) { t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.ID, rbac.ExpandableScope(rbac.ScopeAll)) require.NoError(t, err) @@ -717,6 +717,7 @@ func TestGetAuthorizedWorkspacesAndAgentsByOwnerID(t *testing.T) { t.Run("dbauthz", func(t *testing.T) { t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) authzdb := dbauthz.New(db, authorizer, slogtest.Make(t, &slogtest.Options{}), coderdtest.AccessControlStorePointer()) From 47f9a8aeb8d7e7b318b082f739e3c92fe1a5d09e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:14:51 -0300 Subject: [PATCH 025/223] chore: bump eslint-config-next from 14.2.14 to 14.2.16 in /offlinedocs (#15327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 14.2.14 to 14.2.16.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eslint-config-next&package-manager=npm_and_yarn&previous-version=14.2.14&new-version=14.2.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- offlinedocs/package.json | 2 +- offlinedocs/pnpm-lock.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/offlinedocs/package.json b/offlinedocs/package.json index 4a59c95049f96..28aad66e9cb90 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -34,7 +34,7 @@ "@types/react": "18.3.11", "@types/react-dom": "18.3.0", "eslint": "8.57.1", - "eslint-config-next": "14.2.14", + "eslint-config-next": "14.2.16", "prettier": "3.3.3", "typescript": "5.6.2" }, diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index 20761d3c4ba6f..705a2cc01387b 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -67,8 +67,8 @@ importers: specifier: 8.57.1 version: 8.57.1 eslint-config-next: - specifier: 14.2.14 - version: 14.2.14(eslint@8.57.1)(typescript@5.6.2) + specifier: 14.2.16 + version: 14.2.16(eslint@8.57.1)(typescript@5.6.2) prettier: specifier: 3.3.3 version: 3.3.3 @@ -278,8 +278,8 @@ packages: '@next/env@14.2.14': resolution: {integrity: sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg==} - '@next/eslint-plugin-next@14.2.14': - resolution: {integrity: sha512-kV+OsZ56xhj0rnTn6HegyTGkoa16Mxjrpk7pjWumyB2P8JVQb8S9qtkjy/ye0GnTr4JWtWG4x/2qN40lKZ3iVQ==} + '@next/eslint-plugin-next@14.2.16': + resolution: {integrity: sha512-noORwKUMkKc96MWjTOwrsUCjky0oFegHbeJ1yEnQBGbMHAaTEIgLZIIfsYF0x3a06PiS+2TXppfifR+O6VWslg==} '@next/swc-darwin-arm64@14.2.14': resolution: {integrity: sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==} @@ -866,8 +866,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@14.2.14: - resolution: {integrity: sha512-TXwyjGICAlWC9O0OufS3koTsBKQH8l1xt3SY/aDuvtKHIwjTHplJKWVb1WOEX0OsDaxGbFXmfD2EY1sNfG0Y/w==} + eslint-config-next@14.2.16: + resolution: {integrity: sha512-HOcnCJsyLXR7B8wmjaCgkTSpz+ijgOyAkP8OlvANvciP8PspBYFEBTmakNMxOf71fY0aKOm/blFIiKnrM4K03Q==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 typescript: '>=3.3.1' @@ -2634,7 +2634,7 @@ snapshots: '@next/env@14.2.14': {} - '@next/eslint-plugin-next@14.2.14': + '@next/eslint-plugin-next@14.2.16': dependencies: glob: 10.3.10 @@ -3292,9 +3292,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@14.2.14(eslint@8.57.1)(typescript@5.6.2): + eslint-config-next@14.2.16(eslint@8.57.1)(typescript@5.6.2): dependencies: - '@next/eslint-plugin-next': 14.2.14 + '@next/eslint-plugin-next': 14.2.16 '@rushstack/eslint-patch': 1.5.1 '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.2) From 854044e811de2f2a5e535d03525ef60c55173801 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 1 Nov 2024 11:05:49 -0400 Subject: [PATCH 026/223] chore: fix concurrent `CommitQuota` transactions for unrelated users/orgs (#15261) The failure condition being fixed is `w1` and `w2` could belong to different users, organizations, and templates and still cause a serializable failure if run concurrently. This is because the old query did a `seq scan` on the `workspace_builds` table. Since that is the table being updated, we really want to prevent that. So before this would fail for any 2 workspaces. Now it only fails if `w1` and `w2` are owned by the same user and organization. --- coderd/database/db.go | 33 +- coderd/database/dbauthz/dbauthz.go | 4 + coderd/database/dbauthz/dbauthz_test.go | 5 +- coderd/database/dbauthz/setup_test.go | 1 + coderd/database/dbfake/builder.go | 127 +++++ coderd/database/dbgen/dbgen.go | 2 + coderd/database/dbmem/dbmem.go | 4 + coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 + coderd/database/dbtestutil/db.go | 3 +- coderd/database/dbtestutil/tx.go | 73 +++ coderd/database/pglocks.go | 119 +++++ coderd/database/queries.sql.go | 26 +- coderd/database/queries/quotas.sql | 26 +- enterprise/coderd/workspacequota_test.go | 560 ++++++++++++++++++++++ 15 files changed, 982 insertions(+), 23 deletions(-) create mode 100644 coderd/database/dbfake/builder.go create mode 100644 coderd/database/dbtestutil/tx.go create mode 100644 coderd/database/pglocks.go diff --git a/coderd/database/db.go b/coderd/database/db.go index ae2c31a566cb3..0f923a861efb4 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -28,6 +28,7 @@ type Store interface { wrapper Ping(ctx context.Context) (time.Duration, error) + PGLocks(ctx context.Context) (PGLocks, error) InTx(func(Store) error, *TxOptions) error } @@ -48,13 +49,26 @@ type DBTX interface { GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } +func WithSerialRetryCount(count int) func(*sqlQuerier) { + return func(q *sqlQuerier) { + q.serialRetryCount = count + } +} + // New creates a new database store using a SQL database connection. -func New(sdb *sql.DB) Store { +func New(sdb *sql.DB, opts ...func(*sqlQuerier)) Store { dbx := sqlx.NewDb(sdb, "postgres") - return &sqlQuerier{ + q := &sqlQuerier{ db: dbx, sdb: dbx, + // This is an arbitrary number. + serialRetryCount: 3, + } + + for _, opt := range opts { + opt(q) } + return q } // TxOptions is used to pass some execution metadata to the callers. @@ -104,6 +118,10 @@ type querier interface { type sqlQuerier struct { sdb *sqlx.DB db DBTX + + // serialRetryCount is the number of times to retry a transaction + // if it fails with a serialization error. + serialRetryCount int } func (*sqlQuerier) Wrappers() []string { @@ -143,11 +161,9 @@ func (q *sqlQuerier) InTx(function func(Store) error, txOpts *TxOptions) error { // If we are in a transaction already, the parent InTx call will handle the retry. // We do not want to duplicate those retries. if !inTx && sqlOpts.Isolation == sql.LevelSerializable { - // This is an arbitrarily chosen number. - const retryAmount = 3 var err error attempts := 0 - for attempts = 0; attempts < retryAmount; attempts++ { + for attempts = 0; attempts < q.serialRetryCount; attempts++ { txOpts.executionCount++ err = q.runTx(function, sqlOpts) if err == nil { @@ -203,3 +219,10 @@ func (q *sqlQuerier) runTx(function func(Store) error, txOpts *sql.TxOptions) er } return nil } + +func safeString(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 630e5e6165c6c..c855d5a1984df 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -627,6 +627,10 @@ func (q *querier) Ping(ctx context.Context) (time.Duration, error) { return q.db.Ping(ctx) } +func (q *querier) PGLocks(ctx context.Context) (database.PGLocks, error) { + return q.db.PGLocks(ctx) +} + // InTx runs the given function in a transaction. func (q *querier) InTx(function func(querier database.Store) error, txOpts *database.TxOptions) error { return q.db.InTx(func(tx database.Store) error { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 515330f2edefb..774389d46b9b3 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -152,7 +152,10 @@ func TestDBAuthzRecursive(t *testing.T) { for i := 2; i < method.Type.NumIn(); i++ { ins = append(ins, reflect.New(method.Type.In(i)).Elem()) } - if method.Name == "InTx" || method.Name == "Ping" || method.Name == "Wrappers" { + if method.Name == "InTx" || + method.Name == "Ping" || + method.Name == "Wrappers" || + method.Name == "PGLocks" { continue } // Log the name of the last method, so if there is a panic, it is diff --git a/coderd/database/dbauthz/setup_test.go b/coderd/database/dbauthz/setup_test.go index df9d551101a25..52e8dd42fea9c 100644 --- a/coderd/database/dbauthz/setup_test.go +++ b/coderd/database/dbauthz/setup_test.go @@ -34,6 +34,7 @@ var errMatchAny = xerrors.New("match any error") var skipMethods = map[string]string{ "InTx": "Not relevant", "Ping": "Not relevant", + "PGLocks": "Not relevant", "Wrappers": "Not relevant", "AcquireLock": "Not relevant", "TryAcquireLock": "Not relevant", diff --git a/coderd/database/dbfake/builder.go b/coderd/database/dbfake/builder.go new file mode 100644 index 0000000000000..6803374e72445 --- /dev/null +++ b/coderd/database/dbfake/builder.go @@ -0,0 +1,127 @@ +package dbfake + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/testutil" +) + +type OrganizationBuilder struct { + t *testing.T + db database.Store + seed database.Organization + allUsersAllowance int32 + members []uuid.UUID + groups map[database.Group][]uuid.UUID +} + +func Organization(t *testing.T, db database.Store) OrganizationBuilder { + return OrganizationBuilder{ + t: t, + db: db, + members: []uuid.UUID{}, + groups: make(map[database.Group][]uuid.UUID), + } +} + +type OrganizationResponse struct { + Org database.Organization + AllUsersGroup database.Group + Members []database.OrganizationMember + Groups []database.Group +} + +func (b OrganizationBuilder) EveryoneAllowance(allowance int) OrganizationBuilder { + //nolint: revive // returns modified struct + b.allUsersAllowance = int32(allowance) + return b +} + +func (b OrganizationBuilder) Seed(seed database.Organization) OrganizationBuilder { + //nolint: revive // returns modified struct + b.seed = seed + return b +} + +func (b OrganizationBuilder) Members(users ...database.User) OrganizationBuilder { + for _, u := range users { + //nolint: revive // returns modified struct + b.members = append(b.members, u.ID) + } + return b +} + +func (b OrganizationBuilder) Group(seed database.Group, members ...database.User) OrganizationBuilder { + //nolint: revive // returns modified struct + b.groups[seed] = []uuid.UUID{} + for _, u := range members { + //nolint: revive // returns modified struct + b.groups[seed] = append(b.groups[seed], u.ID) + } + return b +} + +func (b OrganizationBuilder) Do() OrganizationResponse { + org := dbgen.Organization(b.t, b.db, b.seed) + + ctx := testutil.Context(b.t, testutil.WaitShort) + //nolint:gocritic // builder code needs perms + ctx = dbauthz.AsSystemRestricted(ctx) + everyone, err := b.db.InsertAllUsersGroup(ctx, org.ID) + require.NoError(b.t, err) + + if b.allUsersAllowance > 0 { + everyone, err = b.db.UpdateGroupByID(ctx, database.UpdateGroupByIDParams{ + Name: everyone.Name, + DisplayName: everyone.DisplayName, + AvatarURL: everyone.AvatarURL, + QuotaAllowance: b.allUsersAllowance, + ID: everyone.ID, + }) + require.NoError(b.t, err) + } + + members := make([]database.OrganizationMember, 0) + if len(b.members) > 0 { + for _, u := range b.members { + newMem := dbgen.OrganizationMember(b.t, b.db, database.OrganizationMember{ + UserID: u, + OrganizationID: org.ID, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + Roles: nil, + }) + members = append(members, newMem) + } + } + + groups := make([]database.Group, 0) + if len(b.groups) > 0 { + for g, users := range b.groups { + g.OrganizationID = org.ID + group := dbgen.Group(b.t, b.db, g) + groups = append(groups, group) + + for _, u := range users { + dbgen.GroupMember(b.t, b.db, database.GroupMemberTable{ + UserID: u, + GroupID: group.ID, + }) + } + } + } + + return OrganizationResponse{ + Org: org, + AllUsersGroup: everyone, + Members: members, + Groups: groups, + } +} diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 3df873cdb4cbf..4ac675309f662 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -408,6 +408,8 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat } func Group(t testing.TB, db database.Store, orig database.Group) database.Group { + t.Helper() + name := takeFirst(orig.Name, testutil.GetRandomName(t)) group, err := db.InsertGroup(genCtx, database.InsertGroupParams{ ID: takeFirst(orig.ID, uuid.New()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8214a9f6b77ff..67d4373cb7077 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -339,6 +339,10 @@ func (*FakeQuerier) Ping(_ context.Context) (time.Duration, error) { return 0, nil } +func (*FakeQuerier) PGLocks(_ context.Context) (database.PGLocks, error) { + return []database.PGLock{}, nil +} + func (tx *fakeTx) AcquireLock(_ context.Context, id int64) error { if _, ok := tx.FakeQuerier.locks[id]; ok { return xerrors.Errorf("cannot acquire lock %d: already held", id) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 2d542be1160fd..cee25e482bbaa 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -66,6 +66,13 @@ func (m queryMetricsStore) Ping(ctx context.Context) (time.Duration, error) { return duration, err } +func (m queryMetricsStore) PGLocks(ctx context.Context) (database.PGLocks, error) { + start := time.Now() + locks, err := m.s.PGLocks(ctx) + m.queryLatencies.WithLabelValues("PGLocks").Observe(time.Since(start).Seconds()) + return locks, err +} + func (m queryMetricsStore) InTx(f func(database.Store) error, options *database.TxOptions) error { return m.dbMetrics.InTx(f, options) } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 39e82f2e82df5..d8721f56d3f4e 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4329,6 +4329,21 @@ func (mr *MockStoreMockRecorder) OrganizationMembers(arg0, arg1 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationMembers", reflect.TypeOf((*MockStore)(nil).OrganizationMembers), arg0, arg1) } +// PGLocks mocks base method. +func (m *MockStore) PGLocks(arg0 context.Context) (database.PGLocks, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PGLocks", arg0) + ret0, _ := ret[0].(database.PGLocks) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PGLocks indicates an expected call of PGLocks. +func (mr *MockStoreMockRecorder) PGLocks(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PGLocks", reflect.TypeOf((*MockStore)(nil).PGLocks), arg0) +} + // Ping mocks base method. func (m *MockStore) Ping(arg0 context.Context) (time.Duration, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dbtestutil/db.go b/coderd/database/dbtestutil/db.go index 327d880f69648..bc8c571795629 100644 --- a/coderd/database/dbtestutil/db.go +++ b/coderd/database/dbtestutil/db.go @@ -135,7 +135,8 @@ func NewDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub) { if o.dumpOnFailure { t.Cleanup(func() { DumpOnFailure(t, connectionURL) }) } - db = database.New(sqlDB) + // Unit tests should not retry serial transaction failures. + db = database.New(sqlDB, database.WithSerialRetryCount(1)) ps, err = pubsub.New(context.Background(), o.logger, sqlDB, connectionURL) require.NoError(t, err) diff --git a/coderd/database/dbtestutil/tx.go b/coderd/database/dbtestutil/tx.go new file mode 100644 index 0000000000000..15be63dc35aeb --- /dev/null +++ b/coderd/database/dbtestutil/tx.go @@ -0,0 +1,73 @@ +package dbtestutil + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" +) + +type DBTx struct { + database.Store + mu sync.Mutex + done chan error + finalErr chan error +} + +// StartTx starts a transaction and returns a DBTx object. This allows running +// 2 transactions concurrently in a test more easily. +// Example: +// +// a := StartTx(t, db, opts) +// b := StartTx(t, db, opts) +// +// a.GetUsers(...) +// b.GetUsers(...) +// +// require.NoError(t, a.Done() +func StartTx(t *testing.T, db database.Store, opts *database.TxOptions) *DBTx { + done := make(chan error) + finalErr := make(chan error) + txC := make(chan database.Store) + + go func() { + t.Helper() + once := sync.Once{} + count := 0 + + err := db.InTx(func(store database.Store) error { + // InTx can be retried + once.Do(func() { + txC <- store + }) + count++ + if count > 1 { + // If you recursively call InTx, then don't use this. + t.Logf("InTx called more than once: %d", count) + assert.NoError(t, xerrors.New("InTx called more than once, this is not allowed with the StartTx helper")) + } + + <-done + // Just return nil. The caller should be checking their own errors. + return nil + }, opts) + finalErr <- err + }() + + txStore := <-txC + close(txC) + + return &DBTx{Store: txStore, done: done, finalErr: finalErr} +} + +// Done can only be called once. If you call it twice, it will panic. +func (tx *DBTx) Done() error { + tx.mu.Lock() + defer tx.mu.Unlock() + + close(tx.done) + return <-tx.finalErr +} diff --git a/coderd/database/pglocks.go b/coderd/database/pglocks.go new file mode 100644 index 0000000000000..85e1644b3825c --- /dev/null +++ b/coderd/database/pglocks.go @@ -0,0 +1,119 @@ +package database + +import ( + "context" + "fmt" + "reflect" + "sort" + "strings" + "time" + + "github.com/jmoiron/sqlx" + + "github.com/coder/coder/v2/coderd/util/slice" +) + +// PGLock docs see: https://www.postgresql.org/docs/current/view-pg-locks.html#VIEW-PG-LOCKS +type PGLock struct { + // LockType see: https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-LOCK-TABLE + LockType *string `db:"locktype"` + Database *string `db:"database"` // oid + Relation *string `db:"relation"` // oid + RelationName *string `db:"relation_name"` + Page *int `db:"page"` + Tuple *int `db:"tuple"` + VirtualXID *string `db:"virtualxid"` + TransactionID *string `db:"transactionid"` // xid + ClassID *string `db:"classid"` // oid + ObjID *string `db:"objid"` // oid + ObjSubID *int `db:"objsubid"` + VirtualTransaction *string `db:"virtualtransaction"` + PID int `db:"pid"` + Mode *string `db:"mode"` + Granted bool `db:"granted"` + FastPath *bool `db:"fastpath"` + WaitStart *time.Time `db:"waitstart"` +} + +func (l PGLock) Equal(b PGLock) bool { + // Lazy, but hope this works + return reflect.DeepEqual(l, b) +} + +func (l PGLock) String() string { + granted := "granted" + if !l.Granted { + granted = "waiting" + } + var details string + switch safeString(l.LockType) { + case "relation": + details = "" + case "page": + details = fmt.Sprintf("page=%d", *l.Page) + case "tuple": + details = fmt.Sprintf("page=%d tuple=%d", *l.Page, *l.Tuple) + case "virtualxid": + details = "waiting to acquire virtual tx id lock" + default: + details = "???" + } + return fmt.Sprintf("%d-%5s [%s] %s/%s/%s: %s", + l.PID, + safeString(l.TransactionID), + granted, + safeString(l.RelationName), + safeString(l.LockType), + safeString(l.Mode), + details, + ) +} + +// PGLocks returns a list of all locks in the database currently in use. +func (q *sqlQuerier) PGLocks(ctx context.Context) (PGLocks, error) { + rows, err := q.sdb.QueryContext(ctx, ` + SELECT + relation::regclass AS relation_name, + * + FROM pg_locks; + `) + if err != nil { + return nil, err + } + + defer rows.Close() + + var locks []PGLock + err = sqlx.StructScan(rows, &locks) + if err != nil { + return nil, err + } + + return locks, err +} + +type PGLocks []PGLock + +func (l PGLocks) String() string { + // Try to group things together by relation name. + sort.Slice(l, func(i, j int) bool { + return safeString(l[i].RelationName) < safeString(l[j].RelationName) + }) + + var out strings.Builder + for i, lock := range l { + if i != 0 { + _, _ = out.WriteString("\n") + } + _, _ = out.WriteString(lock.String()) + } + return out.String() +} + +// Difference returns the difference between two sets of locks. +// This is helpful to determine what changed between the two sets. +func (l PGLocks) Difference(to PGLocks) (new PGLocks, removed PGLocks) { + return slice.SymmetricDifferenceFunc(l, to, func(a, b PGLock) bool { + return a.Equal(b) + }) +} diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e72db60f3b051..b983d0e1bcd9d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6736,23 +6736,33 @@ const getQuotaConsumedForUser = `-- name: GetQuotaConsumedForUser :one WITH latest_builds AS ( SELECT DISTINCT ON - (workspace_id) id, - workspace_id, - daily_cost + (wb.workspace_id) wb.workspace_id, + wb.daily_cost FROM workspace_builds wb + -- This INNER JOIN prevents a seq scan of the workspace_builds table. + -- Limit the rows to the absolute minimum required, which is all workspaces + -- in a given organization for a given user. +INNER JOIN + workspaces on wb.workspace_id = workspaces.id +WHERE + workspaces.owner_id = $1 AND + workspaces.organization_id = $2 ORDER BY - workspace_id, - created_at DESC + wb.workspace_id, + wb.created_at DESC ) SELECT coalesce(SUM(daily_cost), 0)::BIGINT FROM workspaces -JOIN latest_builds ON +INNER JOIN latest_builds ON latest_builds.workspace_id = workspaces.id -WHERE NOT - deleted AND +WHERE + NOT deleted AND + -- We can likely remove these conditions since we check above. + -- But it does not hurt to be defensive and make sure future query changes + -- do not break anything. workspaces.owner_id = $1 AND workspaces.organization_id = $2 ` diff --git a/coderd/database/queries/quotas.sql b/coderd/database/queries/quotas.sql index 48f9209783e4e..7ab6189dfe8a1 100644 --- a/coderd/database/queries/quotas.sql +++ b/coderd/database/queries/quotas.sql @@ -18,23 +18,33 @@ INNER JOIN groups ON WITH latest_builds AS ( SELECT DISTINCT ON - (workspace_id) id, - workspace_id, - daily_cost + (wb.workspace_id) wb.workspace_id, + wb.daily_cost FROM workspace_builds wb + -- This INNER JOIN prevents a seq scan of the workspace_builds table. + -- Limit the rows to the absolute minimum required, which is all workspaces + -- in a given organization for a given user. +INNER JOIN + workspaces on wb.workspace_id = workspaces.id +WHERE + workspaces.owner_id = @owner_id AND + workspaces.organization_id = @organization_id ORDER BY - workspace_id, - created_at DESC + wb.workspace_id, + wb.created_at DESC ) SELECT coalesce(SUM(daily_cost), 0)::BIGINT FROM workspaces -JOIN latest_builds ON +INNER JOIN latest_builds ON latest_builds.workspace_id = workspaces.id -WHERE NOT - deleted AND +WHERE + NOT deleted AND + -- We can likely remove these conditions since we check above. + -- But it does not hurt to be defensive and make sure future query changes + -- do not break anything. workspaces.owner_id = @owner_id AND workspaces.organization_id = @organization_id ; diff --git a/enterprise/coderd/workspacequota_test.go b/enterprise/coderd/workspacequota_test.go index ac4a77eaec8b4..13142f11e5717 100644 --- a/enterprise/coderd/workspacequota_test.go +++ b/enterprise/coderd/workspacequota_test.go @@ -2,11 +2,13 @@ package coderd_test import ( "context" + "database/sql" "encoding/json" "fmt" "net/http" "sync" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -14,6 +16,11 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" @@ -295,6 +302,497 @@ func TestWorkspaceQuota(t *testing.T) { }) } +// nolint:paralleltest,tparallel // Tests must run serially +func TestWorkspaceSerialization(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("Serialization errors only occur in postgres") + } + + db, _ := dbtestutil.NewDB(t) + + user := dbgen.User(t, db, database.User{}) + otherUser := dbgen.User(t, db, database.User{}) + + org := dbfake.Organization(t, db). + EveryoneAllowance(20). + Members(user, otherUser). + Group(database.Group{ + QuotaAllowance: 10, + }, user, otherUser). + Group(database.Group{ + QuotaAllowance: 10, + }, user). + Do() + + otherOrg := dbfake.Organization(t, db). + EveryoneAllowance(20). + Members(user, otherUser). + Group(database.Group{ + QuotaAllowance: 10, + }, user, otherUser). + Group(database.Group{ + QuotaAllowance: 10, + }, user). + Do() + + // TX mixing tests. **DO NOT** run these in parallel. + // The goal here is to mess around with different ordering of + // transactions and queries. + + // UpdateBuildDeadline bumps a workspace deadline while doing a quota + // commit to the same workspace build. + // + // Note: This passes if the interrupt is run before 'GetQuota()' + // Passing orders: + // - BeginTX -> Bump! -> GetQuota -> GetAllowance -> UpdateCost -> EndTx + // - BeginTX -> GetQuota -> GetAllowance -> UpdateCost -> Bump! -> EndTx + t.Run("UpdateBuildDeadline", func(t *testing.T) { + t.Log("Expected to fail. As long as quota & deadline are on the same " + + " table and affect the same row, this will likely always fail.") + + // +------------------------------+------------------+ + // | Begin Tx | | + // +------------------------------+------------------+ + // | GetQuota(user) | | + // +------------------------------+------------------+ + // | | BumpDeadline(w1) | + // +------------------------------+------------------+ + // | GetAllowance(user) | | + // +------------------------------+------------------+ + // | UpdateWorkspaceBuildCost(w1) | | + // +------------------------------+------------------+ + // | CommitTx() | | + // +------------------------------+------------------+ + // pq: could not serialize access due to concurrent update + ctx := testutil.Context(t, testutil.WaitLong) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + bumpDeadline := func() { + err := db.InTx(func(db database.Store) error { + err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{ + Deadline: dbtime.Now(), + MaxDeadline: dbtime.Now(), + UpdatedAt: dbtime.Now(), + ID: myWorkspace.Build.ID, + }) + return err + }, &database.TxOptions{ + Isolation: sql.LevelSerializable, + }) + assert.NoError(t, err) + } + + // Start TX + // Run order + + quota := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + quota.GetQuota(ctx, t) // Step 1 + bumpDeadline() // Interrupt + quota.GetAllowance(ctx, t) // Step 2 + + err := quota.DBTx.UpdateWorkspaceBuildCostByID(ctx, database.UpdateWorkspaceBuildCostByIDParams{ + ID: myWorkspace.Build.ID, + DailyCost: 10, + }) // Step 3 + require.ErrorContains(t, err, "could not serialize access due to concurrent update") + // End commit + require.ErrorContains(t, quota.Done(), "failed transaction") + }) + + // UpdateOtherBuildDeadline bumps a user's other workspace deadline + // while doing a quota commit. + t.Run("UpdateOtherBuildDeadline", func(t *testing.T) { + // +------------------------------+------------------+ + // | Begin Tx | | + // +------------------------------+------------------+ + // | GetQuota(user) | | + // +------------------------------+------------------+ + // | | BumpDeadline(w2) | + // +------------------------------+------------------+ + // | GetAllowance(user) | | + // +------------------------------+------------------+ + // | UpdateWorkspaceBuildCost(w1) | | + // +------------------------------+------------------+ + // | CommitTx() | | + // +------------------------------+------------------+ + // Works! + ctx := testutil.Context(t, testutil.WaitLong) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + // Use the same template + otherWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }). + Seed(database.WorkspaceBuild{ + TemplateVersionID: myWorkspace.TemplateVersion.ID, + }). + Do() + + bumpDeadline := func() { + err := db.InTx(func(db database.Store) error { + err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{ + Deadline: dbtime.Now(), + MaxDeadline: dbtime.Now(), + UpdatedAt: dbtime.Now(), + ID: otherWorkspace.Build.ID, + }) + return err + }, &database.TxOptions{ + Isolation: sql.LevelSerializable, + }) + assert.NoError(t, err) + } + + // Start TX + // Run order + + quota := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + quota.GetQuota(ctx, t) // Step 1 + bumpDeadline() // Interrupt + quota.GetAllowance(ctx, t) // Step 2 + quota.UpdateWorkspaceBuildCostByID(ctx, t, 10) // Step 3 + // End commit + require.NoError(t, quota.Done()) + }) + + t.Run("ActivityBump", func(t *testing.T) { + t.Log("Expected to fail. As long as quota & deadline are on the same " + + " table and affect the same row, this will likely always fail.") + // +---------------------+----------------------------------+ + // | W1 Quota Tx | | + // +---------------------+----------------------------------+ + // | Begin Tx | | + // +---------------------+----------------------------------+ + // | GetQuota(w1) | | + // +---------------------+----------------------------------+ + // | GetAllowance(w1) | | + // +---------------------+----------------------------------+ + // | | ActivityBump(w1) | + // +---------------------+----------------------------------+ + // | UpdateBuildCost(w1) | | + // +---------------------+----------------------------------+ + // | CommitTx() | | + // +---------------------+----------------------------------+ + // pq: could not serialize access due to concurrent update + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }). + Seed(database.WorkspaceBuild{ + // Make sure the bump does something + Deadline: dbtime.Now().Add(time.Hour * -20), + }). + Do() + + one := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + + // Run order + one.GetQuota(ctx, t) + one.GetAllowance(ctx, t) + + err := db.ActivityBumpWorkspace(ctx, database.ActivityBumpWorkspaceParams{ + NextAutostart: time.Now(), + WorkspaceID: myWorkspace.Workspace.ID, + }) + + assert.NoError(t, err) + + err = one.DBTx.UpdateWorkspaceBuildCostByID(ctx, database.UpdateWorkspaceBuildCostByIDParams{ + ID: myWorkspace.Build.ID, + DailyCost: 10, + }) + require.ErrorContains(t, err, "could not serialize access due to concurrent update") + + // End commit + assert.ErrorContains(t, one.Done(), "failed transaction") + }) + + t.Run("BumpLastUsedAt", func(t *testing.T) { + // +---------------------+----------------------------------+ + // | W1 Quota Tx | | + // +---------------------+----------------------------------+ + // | Begin Tx | | + // +---------------------+----------------------------------+ + // | GetQuota(w1) | | + // +---------------------+----------------------------------+ + // | GetAllowance(w1) | | + // +---------------------+----------------------------------+ + // | | UpdateWorkspaceLastUsedAt(w1) | + // +---------------------+----------------------------------+ + // | UpdateBuildCost(w1) | | + // +---------------------+----------------------------------+ + // | CommitTx() | | + // +---------------------+----------------------------------+ + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + one := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + + // Run order + one.GetQuota(ctx, t) + one.GetAllowance(ctx, t) + + err := db.UpdateWorkspaceLastUsedAt(ctx, database.UpdateWorkspaceLastUsedAtParams{ + ID: myWorkspace.Workspace.ID, + LastUsedAt: dbtime.Now(), + }) + assert.NoError(t, err) + + one.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + // End commit + assert.NoError(t, one.Done()) + }) + + t.Run("UserMod", func(t *testing.T) { + // +---------------------+----------------------------------+ + // | W1 Quota Tx | | + // +---------------------+----------------------------------+ + // | Begin Tx | | + // +---------------------+----------------------------------+ + // | GetQuota(w1) | | + // +---------------------+----------------------------------+ + // | GetAllowance(w1) | | + // +---------------------+----------------------------------+ + // | | RemoveUserFromOrg | + // +---------------------+----------------------------------+ + // | UpdateBuildCost(w1) | | + // +---------------------+----------------------------------+ + // | CommitTx() | | + // +---------------------+----------------------------------+ + // Works! + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + var err error + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + one := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + + // Run order + + one.GetQuota(ctx, t) + one.GetAllowance(ctx, t) + + err = db.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{ + OrganizationID: myWorkspace.Workspace.OrganizationID, + UserID: user.ID, + }) + assert.NoError(t, err) + + one.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + // End commit + assert.NoError(t, one.Done()) + }) + + // QuotaCommit 2 workspaces in different orgs. + // Workspaces do not share templates, owners, or orgs + t.Run("DoubleQuotaUnrelatedWorkspaces", func(t *testing.T) { + // +---------------------+---------------------+ + // | W1 Quota Tx | W2 Quota Tx | + // +---------------------+---------------------+ + // | Begin Tx | | + // +---------------------+---------------------+ + // | | Begin Tx | + // +---------------------+---------------------+ + // | GetQuota(w1) | | + // +---------------------+---------------------+ + // | GetAllowance(w1) | | + // +---------------------+---------------------+ + // | UpdateBuildCost(w1) | | + // +---------------------+---------------------+ + // | | UpdateBuildCost(w2) | + // +---------------------+---------------------+ + // | | GetQuota(w2) | + // +---------------------+---------------------+ + // | | GetAllowance(w2) | + // +---------------------+---------------------+ + // | CommitTx() | | + // +---------------------+---------------------+ + // | | CommitTx() | + // +---------------------+---------------------+ + ctx := testutil.Context(t, testutil.WaitLong) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + myOtherWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: otherOrg.Org.ID, // Different org! + OwnerID: otherUser.ID, + }).Do() + + one := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + two := newCommitter(t, db, myOtherWorkspace.Workspace, myOtherWorkspace.Build) + + // Run order + one.GetQuota(ctx, t) + one.GetAllowance(ctx, t) + + one.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + two.GetQuota(ctx, t) + two.GetAllowance(ctx, t) + two.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + // End commit + assert.NoError(t, one.Done()) + assert.NoError(t, two.Done()) + }) + + // QuotaCommit 2 workspaces in different orgs. + // Workspaces do not share templates or orgs + t.Run("DoubleQuotaUserWorkspacesDiffOrgs", func(t *testing.T) { + // +---------------------+---------------------+ + // | W1 Quota Tx | W2 Quota Tx | + // +---------------------+---------------------+ + // | Begin Tx | | + // +---------------------+---------------------+ + // | | Begin Tx | + // +---------------------+---------------------+ + // | GetQuota(w1) | | + // +---------------------+---------------------+ + // | GetAllowance(w1) | | + // +---------------------+---------------------+ + // | UpdateBuildCost(w1) | | + // +---------------------+---------------------+ + // | | UpdateBuildCost(w2) | + // +---------------------+---------------------+ + // | | GetQuota(w2) | + // +---------------------+---------------------+ + // | | GetAllowance(w2) | + // +---------------------+---------------------+ + // | CommitTx() | | + // +---------------------+---------------------+ + // | | CommitTx() | + // +---------------------+---------------------+ + ctx := testutil.Context(t, testutil.WaitLong) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + myOtherWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: otherOrg.Org.ID, // Different org! + OwnerID: user.ID, + }).Do() + + one := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + two := newCommitter(t, db, myOtherWorkspace.Workspace, myOtherWorkspace.Build) + + // Run order + one.GetQuota(ctx, t) + one.GetAllowance(ctx, t) + + one.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + two.GetQuota(ctx, t) + two.GetAllowance(ctx, t) + two.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + // End commit + assert.NoError(t, one.Done()) + assert.NoError(t, two.Done()) + }) + + // QuotaCommit 2 workspaces in the same org. + // Workspaces do not share templates + t.Run("DoubleQuotaUserWorkspaces", func(t *testing.T) { + t.Log("Setting a new build cost to a workspace in a org affects other " + + "workspaces in the same org. This is expected to fail.") + // +---------------------+---------------------+ + // | W1 Quota Tx | W2 Quota Tx | + // +---------------------+---------------------+ + // | Begin Tx | | + // +---------------------+---------------------+ + // | | Begin Tx | + // +---------------------+---------------------+ + // | GetQuota(w1) | | + // +---------------------+---------------------+ + // | GetAllowance(w1) | | + // +---------------------+---------------------+ + // | UpdateBuildCost(w1) | | + // +---------------------+---------------------+ + // | | UpdateBuildCost(w2) | + // +---------------------+---------------------+ + // | | GetQuota(w2) | + // +---------------------+---------------------+ + // | | GetAllowance(w2) | + // +---------------------+---------------------+ + // | CommitTx() | | + // +---------------------+---------------------+ + // | | CommitTx() | + // +---------------------+---------------------+ + // pq: could not serialize access due to read/write dependencies among transactions + ctx := testutil.Context(t, testutil.WaitLong) + //nolint:gocritic // testing + ctx = dbauthz.AsSystemRestricted(ctx) + + myWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + myOtherWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: org.Org.ID, + OwnerID: user.ID, + }).Do() + + one := newCommitter(t, db, myWorkspace.Workspace, myWorkspace.Build) + two := newCommitter(t, db, myOtherWorkspace.Workspace, myOtherWorkspace.Build) + + // Run order + one.GetQuota(ctx, t) + one.GetAllowance(ctx, t) + + one.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + two.GetQuota(ctx, t) + two.GetAllowance(ctx, t) + two.UpdateWorkspaceBuildCostByID(ctx, t, 10) + + // End commit + assert.NoError(t, one.Done()) + assert.ErrorContains(t, two.Done(), "could not serialize access due to read/write dependencies among transactions") + }) +} + func deprecatedQuotaEndpoint(ctx context.Context, client *codersdk.Client, userID string) (codersdk.WorkspaceQuota, error) { res, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspace-quota/%s", userID), nil) if err != nil { @@ -335,3 +833,65 @@ func applyWithCost(cost int32) []*proto.Response { }, }} } + +// committer does what the CommitQuota does, but allows +// stepping through the actions in the tx and controlling the +// timing. +// This is a nice wrapper to make the tests more concise. +type committer struct { + DBTx *dbtestutil.DBTx + w database.WorkspaceTable + b database.WorkspaceBuild +} + +func newCommitter(t *testing.T, db database.Store, workspace database.WorkspaceTable, build database.WorkspaceBuild) *committer { + quotaTX := dbtestutil.StartTx(t, db, &database.TxOptions{ + Isolation: sql.LevelSerializable, + ReadOnly: false, + }) + return &committer{DBTx: quotaTX, w: workspace, b: build} +} + +// GetQuota touches: +// - workspace_builds +// - workspaces +func (c *committer) GetQuota(ctx context.Context, t *testing.T) int64 { + t.Helper() + + consumed, err := c.DBTx.GetQuotaConsumedForUser(ctx, database.GetQuotaConsumedForUserParams{ + OwnerID: c.w.OwnerID, + OrganizationID: c.w.OrganizationID, + }) + require.NoError(t, err) + return consumed +} + +// GetAllowance touches: +// - group_members_expanded +// - users +// - groups +// - org_members +func (c *committer) GetAllowance(ctx context.Context, t *testing.T) int64 { + t.Helper() + + allowance, err := c.DBTx.GetQuotaAllowanceForUser(ctx, database.GetQuotaAllowanceForUserParams{ + UserID: c.w.OwnerID, + OrganizationID: c.w.OrganizationID, + }) + require.NoError(t, err) + return allowance +} + +func (c *committer) UpdateWorkspaceBuildCostByID(ctx context.Context, t *testing.T, cost int32) bool { + t.Helper() + + err := c.DBTx.UpdateWorkspaceBuildCostByID(ctx, database.UpdateWorkspaceBuildCostByIDParams{ + ID: c.b.ID, + DailyCost: cost, + }) + return assert.NoError(t, err) +} + +func (c *committer) Done() error { + return c.DBTx.Done() +} From 85f05ad39621735770f50e00fc4342d8eb685504 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:47:47 -0300 Subject: [PATCH 027/223] chore: bump @types/jest from 29.5.13 to 29.5.14 in /site in the jest group (#15320) --- site/package.json | 2 +- site/pnpm-lock.yaml | 87 ++++++++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/site/package.json b/site/package.json index c154f47a6a8ff..2fc778e319a64 100644 --- a/site/package.json +++ b/site/package.json @@ -126,7 +126,7 @@ "@types/color-convert": "2.0.0", "@types/express": "4.17.17", "@types/file-saver": "2.0.7", - "@types/jest": "29.5.13", + "@types/jest": "29.5.14", "@types/lodash": "4.17.9", "@types/node": "20.16.10", "@types/react": "18.3.11", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 55e74260bb9e6..1e9d96529e315 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -234,7 +234,7 @@ importers: version: 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-interactions': specifier: 8.1.11 - version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) '@storybook/addon-links': specifier: 8.1.11 version: 8.1.11(react@18.3.1) @@ -255,7 +255,7 @@ importers: version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10)) '@storybook/test': specifier: 8.1.11 - version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) '@swc/core': specifier: 1.3.38 version: 1.3.38 @@ -264,7 +264,7 @@ importers: version: 0.2.36(@swc/core@1.3.38) '@testing-library/jest-dom': specifier: 6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) '@testing-library/react': specifier: 14.3.1 version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -287,8 +287,8 @@ importers: specifier: 2.0.7 version: 2.0.7 '@types/jest': - specifier: 29.5.13 - version: 29.5.13 + specifier: 29.5.14 + version: 29.5.14 '@types/lodash': specifier: 4.17.9 version: 4.17.9 @@ -427,6 +427,10 @@ packages: resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.25.8': resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} engines: {node: '>=6.9.0'} @@ -497,6 +501,10 @@ packages: resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.7': resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} engines: {node: '>=6.9.0'} @@ -1221,14 +1229,14 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/eslintrc@2.1.4': @@ -2483,8 +2491,8 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.13': - resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} @@ -2724,8 +2732,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true @@ -4859,6 +4867,9 @@ packages: picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -6111,6 +6122,12 @@ snapshots: '@babel/highlight': 7.25.7 picocolors: 1.1.0 + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.25.8': {} '@babel/core@7.25.8': @@ -6206,6 +6223,8 @@ snapshots: '@babel/helper-validator-identifier@7.25.7': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-option@7.25.7': {} '@babel/helpers@7.25.7': @@ -6768,13 +6787,13 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.52.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@8.52.0)': dependencies: eslint: 8.52.0 eslint-visitor-keys: 3.4.3 optional: true - '@eslint-community/regexpp@4.11.1': + '@eslint-community/regexpp@4.12.1': optional: true '@eslint/eslintrc@2.1.4': @@ -7645,11 +7664,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@storybook/addon-interactions@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.11 - '@storybook/test': 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + '@storybook/test': 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) '@storybook/types': 8.1.11 polished: 4.2.2 ts-dedent: 2.2.0 @@ -8094,14 +8113,14 @@ snapshots: core-js: 3.32.0 find-up: 4.1.0 - '@storybook/test@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@storybook/test@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': dependencies: '@storybook/client-logger': 8.1.11 '@storybook/core-events': 8.1.11 '@storybook/instrumenter': 8.1.11 '@storybook/preview-api': 8.1.11 '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 @@ -8235,7 +8254,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.25.6 @@ -8247,10 +8266,10 @@ snapshots: redent: 3.0.0 optionalDependencies: '@jest/globals': 29.7.0 - '@types/jest': 29.5.13 + '@types/jest': 29.5.14 jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.13)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.7 @@ -8262,7 +8281,7 @@ snapshots: redent: 3.0.0 optionalDependencies: '@jest/globals': 29.7.0 - '@types/jest': 29.5.13 + '@types/jest': 29.5.14 jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) '@testing-library/react-hooks@8.0.1(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -8442,7 +8461,7 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.13': + '@types/jest@29.5.14': dependencies: expect: 29.7.0 pretty-format: 29.7.0 @@ -8670,9 +8689,9 @@ snapshots: dependencies: acorn: 7.4.1 - acorn-jsx@5.3.2(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.12.1 + acorn: 8.14.0 optional: true acorn-walk@7.2.0: {} @@ -8687,7 +8706,7 @@ snapshots: acorn@8.11.2: {} - acorn@8.12.1: + acorn@8.14.0: optional: true agent-base@6.0.2: @@ -9494,8 +9513,8 @@ snapshots: eslint@8.52.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/eslint-utils': 4.4.1(eslint@8.52.0) + '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.52.0 '@humanwhocodes/config-array': 0.11.14 @@ -9538,8 +9557,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) eslint-visitor-keys: 3.4.3 optional: true @@ -10414,7 +10433,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.25.7 + '@babel/code-frame': 7.26.2 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -11336,6 +11355,8 @@ snapshots: picocolors@1.1.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} pirates@4.0.6: {} @@ -12398,7 +12419,7 @@ snapshots: dependencies: browserslist: 4.24.0 escalade: 3.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 uri-js@4.4.1: dependencies: From 8da30a1e599bceaa9f3f383f0b056ea31ae6eb04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:58:14 -0600 Subject: [PATCH 028/223] chore: update typescript from 5.6.2 to 5.6.3 in /offlinedocs (#15323) --- offlinedocs/package.json | 2 +- offlinedocs/pnpm-lock.yaml | 94 +++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/offlinedocs/package.json b/offlinedocs/package.json index 28aad66e9cb90..29dfc5fb96c95 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -36,7 +36,7 @@ "eslint": "8.57.1", "eslint-config-next": "14.2.16", "prettier": "3.3.3", - "typescript": "5.6.2" + "typescript": "5.6.3" }, "engines": { "npm": ">=9.0.0 <10.0.0", diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index 705a2cc01387b..0b47e84558461 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -68,13 +68,13 @@ importers: version: 8.57.1 eslint-config-next: specifier: 14.2.16 - version: 14.2.16(eslint@8.57.1)(typescript@5.6.2) + version: 14.2.16(eslint@8.57.1)(typescript@5.6.3) prettier: specifier: 3.3.3 version: 3.3.3 typescript: - specifier: 5.6.2 - version: 5.6.2 + specifier: 5.6.3 + version: 5.6.3 packages: @@ -2223,8 +2223,8 @@ packages: typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -2749,33 +2749,33 @@ snapshots: '@types/unist@3.0.2': {} - '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/scope-manager': 8.8.0 - '@typescript-eslint/type-utils': 8.8.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/utils': 8.8.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/type-utils': 8.8.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.0(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.8.0 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) debug: 4.3.6 eslint: 8.57.1 optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -2789,14 +2789,14 @@ snapshots: '@typescript-eslint/types': 8.8.0 '@typescript-eslint/visitor-keys': 8.8.0 - '@typescript-eslint/type-utils@8.8.0(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/type-utils@8.8.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.2) - '@typescript-eslint/utils': 8.8.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.0(eslint@8.57.1)(typescript@5.6.3) debug: 4.3.6 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color @@ -2805,7 +2805,7 @@ snapshots: '@typescript-eslint/types@8.8.0': {} - '@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.2)': + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 @@ -2813,13 +2813,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.6.2) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.8.0(typescript@5.6.2)': + '@typescript-eslint/typescript-estree@8.8.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 8.8.0 '@typescript-eslint/visitor-keys': 8.8.0 @@ -2828,18 +2828,18 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.8.0(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/utils@8.8.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) '@typescript-eslint/scope-manager': 8.8.0 '@typescript-eslint/types': 8.8.0 - '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color @@ -3292,21 +3292,21 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@14.2.16(eslint@8.57.1)(typescript@5.6.2): + eslint-config-next@14.2.16(eslint@8.57.1)(typescript@5.6.3): dependencies: '@next/eslint-plugin-next': 14.2.16 '@rushstack/eslint-patch': 1.5.1 - '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1) - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.1) eslint-plugin-react: 7.33.2(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.1) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint-import-resolver-webpack - supports-color @@ -3319,13 +3319,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1): + eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1): dependencies: debug: 4.3.6 enhanced-resolve: 5.15.0 eslint: 8.57.1 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1) get-tsconfig: 4.6.2 globby: 13.2.2 is-core-module: 2.15.1 @@ -3337,18 +3337,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1): + eslint-plugin-import@2.28.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1): dependencies: array-includes: 3.1.6 array.prototype.findlastindex: 1.2.3 @@ -3358,7 +3358,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.28.1)(eslint@8.57.1))(eslint@8.57.1) has: 1.0.3 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -3369,7 +3369,7 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.14.2 optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5005,9 +5005,9 @@ snapshots: trough@2.1.0: {} - ts-api-utils@1.3.0(typescript@5.6.2): + ts-api-utils@1.3.0(typescript@5.6.3): dependencies: - typescript: 5.6.2 + typescript: 5.6.3 tsconfig-paths@3.14.2: dependencies: @@ -5022,10 +5022,10 @@ snapshots: tslib@2.6.2: {} - tsutils@3.21.0(typescript@5.6.2): + tsutils@3.21.0(typescript@5.6.3): dependencies: tslib: 1.14.1 - typescript: 5.6.2 + typescript: 5.6.3 type-check@0.4.0: dependencies: @@ -5060,7 +5060,7 @@ snapshots: for-each: 0.3.3 is-typed-array: 1.1.12 - typescript@5.6.2: {} + typescript@5.6.3: {} unbox-primitive@1.0.2: dependencies: From 13b97cf3dc9d71cb05a08e2b6e8caa8d34754336 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:58:48 -0600 Subject: [PATCH 029/223] chore: bump @storybook/addon-actions from 8.1.11 to 8.3.5 in /site (#15330) --- site/package.json | 2 +- site/pnpm-lock.yaml | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/site/package.json b/site/package.json index 2fc778e319a64..d404d75e800d3 100644 --- a/site/package.json +++ b/site/package.json @@ -106,7 +106,7 @@ "@chromatic-com/storybook": "1.9.0", "@octokit/types": "12.3.0", "@playwright/test": "1.47.2", - "@storybook/addon-actions": "8.1.11", + "@storybook/addon-actions": "8.3.5", "@storybook/addon-essentials": "8.1.11", "@storybook/addon-interactions": "8.1.11", "@storybook/addon-links": "8.1.11", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 1e9d96529e315..99f5d8d81ffa4 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -227,8 +227,8 @@ importers: specifier: 1.47.2 version: 1.47.2 '@storybook/addon-actions': - specifier: 8.1.11 - version: 8.1.11 + specifier: 8.3.5 + version: 8.3.5(storybook@8.3.5) '@storybook/addon-essentials': specifier: 8.1.11 version: 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1952,6 +1952,11 @@ packages: '@storybook/addon-actions@8.1.11': resolution: {integrity: sha512-jqYXgBgOVInStOCk//AA+dGkrfN8R7rDXA4lyu82zM59kvICtG9iqgmkSRDn0Z3zUkM+lIHZGoz0aLVQ8pxsgw==} + '@storybook/addon-actions@8.3.5': + resolution: {integrity: sha512-t8D5oo+4XfD+F8091wLa2y/CDd/W2lExCeol5Vm1tp5saO+u6f2/d7iykLhTowWV84Uohi3D073uFeyTAlGebg==} + peerDependencies: + storybook: ^8.3.5 + '@storybook/addon-backgrounds@8.1.11': resolution: {integrity: sha512-naGf1ovmsU2pSWb270yRO1IidnO+0YCZ5Tcb8I4rPhZ0vsdXNURYKS1LPSk1OZkvaUXdeB4Im9HhHfUBJOW9oQ==} @@ -7586,6 +7591,15 @@ snapshots: polished: 4.2.2 uuid: 9.0.1 + '@storybook/addon-actions@8.3.5(storybook@8.3.5)': + dependencies: + '@storybook/global': 5.0.0 + '@types/uuid': 9.0.2 + dequal: 2.0.3 + polished: 4.2.2 + storybook: 8.3.5 + uuid: 9.0.1 + '@storybook/addon-backgrounds@8.1.11': dependencies: '@storybook/global': 5.0.0 From 18ef954a037917c93f8851d4cb6a7ee0321faeb6 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Fri, 1 Nov 2024 12:24:35 -0400 Subject: [PATCH 030/223] docs: add new best practice doc to speed up templates and workspaces (#15296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/coder/coder/issues/14858 - [x] TODO: update `build-timeline.png` - [x] TODO: `Best practices` doesn't show up in the sidebar until you're actually in the doc 🤔 [preview](https://coder.com/docs/@bp-speed-up-templates/tutorials/best-practices/speed-up-templates) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Co-authored-by: Muhammad Atif Ali Co-authored-by: Ben Potter --- docs/images/best-practice/build-timeline.png | Bin 0 -> 112612 bytes docs/install/kubernetes.md | 46 +++--- docs/manifest.json | 12 ++ docs/tutorials/best-practices/index.md | 5 + .../best-practices/speed-up-templates.md | 143 ++++++++++++++++++ 5 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 docs/images/best-practice/build-timeline.png create mode 100644 docs/tutorials/best-practices/index.md create mode 100644 docs/tutorials/best-practices/speed-up-templates.md diff --git a/docs/images/best-practice/build-timeline.png b/docs/images/best-practice/build-timeline.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1c1191ee7ccca57309218ea2e9ca74990760b4 GIT binary patch literal 112612 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 V$pjQH`Li$&69Y5-VqKSr{{U11y?_7! literal 0 HcmV?d00001 diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md index 600881ec0289f..94483b7c40b57 100644 --- a/docs/install/kubernetes.md +++ b/docs/install/kubernetes.md @@ -1,6 +1,6 @@ # Install Coder on Kubernetes -You can install Coder on Kubernetes using Helm. We run on most Kubernetes +You can install Coder on Kubernetes (K8s) using Helm. We run on most Kubernetes distributions, including [OpenShift](./openshift.md). ## Requirements @@ -121,27 +121,27 @@ coder: We support two release channels: mainline and stable - read the [Releases](./releases.md) page to learn more about which best suits your team. -For the **mainline** Coder release: +- **Mainline** Coder release: - + -```shell -helm install coder coder-v2/coder \ - --namespace coder \ - --values values.yaml \ - --version 2.15.0 -``` + ```shell + helm install coder coder-v2/coder \ + --namespace coder \ + --values values.yaml \ + --version 2.15.0 + ``` - For the **stable** Coder release: +- **Stable** Coder release: - + -```shell -helm install coder coder-v2/coder \ - --namespace coder \ - --values values.yaml \ - --version 2.15.1 -``` + ```shell + helm install coder coder-v2/coder \ + --namespace coder \ + --values values.yaml \ + --version 2.15.1 + ``` You can watch Coder start up by running `kubectl get pods -n coder`. Once Coder has started, the `coder-*` pods should enter the `Running` state. @@ -167,6 +167,18 @@ helm upgrade coder coder-v2/coder \ -f values.yaml ``` +## Coder Observability Chart + +Use the [Observability Helm chart](https://github.com/coder/observability) for a +pre-built set of dashboards to monitor your control plane over time. It includes +Grafana, Prometheus, Loki, and Alert Manager out-of-the-box, and can be deployed +on your existing Grafana instance. + +We recommend that all administrators deploying on Kubernetes set the +observability bundle up with the control plane from the start. For installation +instructions, visit the +[observability repository](https://github.com/coder/observability?tab=readme-ov-file#installation). + ## Kubernetes Security Reference Below are common requirements we see from our enterprise customers when diff --git a/docs/manifest.json b/docs/manifest.json index 05f4d5d3a7680..10f0cda33f1e9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -723,6 +723,18 @@ "title": "FAQs", "description": "Miscellaneous FAQs from our community", "path": "./tutorials/faqs.md" + }, + { + "title": "Best practices", + "description": "Guides to help you make the most of your Coder experience", + "path": "./tutorials/best-practices/index.md", + "children": [ + { + "title": "Speed up your workspaces", + "description": "Speed up your Coder templates and workspaces", + "path": "./tutorials/best-practices/speed-up-templates.md" + } + ] } ] }, diff --git a/docs/tutorials/best-practices/index.md b/docs/tutorials/best-practices/index.md new file mode 100644 index 0000000000000..ccc12f61e5a92 --- /dev/null +++ b/docs/tutorials/best-practices/index.md @@ -0,0 +1,5 @@ +# Best practices + +Guides to help you make the most of your Coder experience. + + diff --git a/docs/tutorials/best-practices/speed-up-templates.md b/docs/tutorials/best-practices/speed-up-templates.md new file mode 100644 index 0000000000000..ddf08b5e51d75 --- /dev/null +++ b/docs/tutorials/best-practices/speed-up-templates.md @@ -0,0 +1,143 @@ +# Speed up your Coder templates and workspaces + +October 31, 2024 + +--- + +If it takes your workspace a long time to start, find out why and make some +changes to your Coder templates to help speed things up. + +## Monitoring + +You can monitor [Coder logs](../../admin/monitoring/logs.md) through the +system-native tools on your deployment platform, or stream logs to tools like +Splunk, Datadog, Grafana Loki, and others. + +### Workspace build timeline + +Use the **Build timeline** to monitor the time it takes to start specific +workspaces. Identify long scripts, resources, and other things you can +potentially optimize within the template. + +![Screenshot of a workspace and its build timeline](../../images/best-practice/build-timeline.png) + +Adjust this request to match your Coder access URL and workspace: + +```shell +curl -X GET https://coder.example.com/api/v2/workspacebuilds/{workspacebuild}/timings \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +Visit the +[API documentation](../../reference/api/builds.md#get-workspace-build-timings-by-id) +for more information. + +### Coder Observability Chart + +Use the [Observability Helm chart](https://github.com/coder/observability) for a +pre-built set of dashboards to monitor your control plane over time. It includes +Grafana, Prometheus, Loki, and Alert Manager out-of-the-box, and can be deployed +on your existing Grafana instance. + +We recommend that all administrators deploying on Kubernetes or on an existing +Prometheus or Grafana stack set the observability bundle up with the control +plane from the start. For installation instructions, visit the +[observability repository](https://github.com/coder/observability?tab=readme-ov-file#installation), +or our [Kubernetes installation guide](../../install/kubernetes.md). + +### Enable Prometheus metrics for Coder + +[Prometheus.io](https://prometheus.io/docs/introduction/overview/#what-is-prometheus) +is included as part of the [observability chart](#coder-observability-chart). It +offers a variety of +[available metrics](../../admin/integrations/prometheus.md#available-metrics), +such as `coderd_provisionerd_job_timings_seconds` and +`coderd_agentstats_startup_script_seconds`, which measure how long the workspace +takes to provision and how long the startup script takes. + +You can +[install it separately](https://prometheus.io/docs/prometheus/latest/getting_started/) +if you prefer. + +## Provisioners + +`coder server` defaults to three provisioner daemons. Each provisioner daemon +can handle one single job, such as start, stop, or delete at a time and can be +resource intensive. When all provisioners are busy, workspaces enter a "pending" +state until a provisioner becomes available. + +### Increase provisioner daemons + +Provisioners are queue-based to reduce unpredictable load to the Coder server. +However, they can be scaled up to allow more concurrent provisioners. You risk +overloading the central Coder server if you use too many built-in provisioners, +so we recommend a maximum of five provisioners. For more than five provisioners, +we recommend that you move to +[external provisioners](../../admin/provisioners.md). + +If you can’t move to external provisioners, use the `provisioner-daemons` flag +to increase the number of provisioner daemons to five: + +```shell +coder server --provisioner-daemons=5 +``` + +Visit the +[CLI documentation](../../reference/cli/server.md#--provisioner-daemons) for +more information about increasing provisioner daemons, configuring external +provisioners, and other options. + +### Adjust provisioner CPU/memory + +We recommend that you deploy Coder to its own respective Kubernetes cluster, +separate from production applications. Keep in mind that Coder runs development +workloads, so the cluster should be deployed as such, without production-level +configurations. + +Adjust the CPU and memory values as shown in +[Helm provisioner values.yaml](https://github.com/coder/coder/blob/main/helm/provisioner/values.yaml#L134-L141): + +```yaml +… + resources: + limits: + cpu: "0.25" + memory: "1Gi" + requests: + cpu: "0.25" + memory: "1Gi" +… +``` + +Visit the +[validated architecture documentation](../../admin/infrastructure/validated-architectures/index.md#workspace-nodes) +for more information. + +## Set up Terraform provider caching + +By default, Coder downloads each Terraform provider when a workspace starts. +This can create unnecessary network and disk I/O. + +`terraform init` generates a `.terraform.lock.hcl` which instructs Coder +provisioners to cache specific versions of your providers. + +To use `terraform init` to cache providers: + +1. Pull the templates to your local device: + + ```shell + coder templates pull + ``` + +1. Run `terraform init` to initialize the directory: + + ```shell + terraform init + ``` + +1. Push the templates back to your Coder deployment: + + ```shell + coder templates push + ``` From e232aee011da2d14ae378f4e068d368f6dce845f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 1 Nov 2024 13:29:00 -0300 Subject: [PATCH 031/223] feat(site): add agent connection timings (#15276) Local preview: Screenshot 2024-10-29 at 16 16 01 Close https://github.com/coder/internal/issues/116 --------- Co-authored-by: Danny Kopping --- coderd/apidoc/docs.go | 62 ++++- coderd/apidoc/swagger.json | 62 ++++- coderd/database/dbauthz/dbauthz_test.go | 16 +- coderd/database/dbmem/dbmem.go | 30 +- coderd/database/queries.sql.go | 24 +- coderd/database/queries/workspaceagents.sql | 6 +- coderd/workspacebuilds.go | 43 ++- coderd/workspacebuilds_test.go | 117 ++++++-- codersdk/workspacebuilds.go | 57 +++- docs/reference/api/builds.md | 17 +- docs/reference/api/schemas.md | 113 ++++++-- docs/reference/api/workspaces.md | 17 +- site/src/api/queries/workspaceBuilds.ts | 13 +- site/src/api/typesGenerated.ts | 20 +- .../WorkspaceTiming/ResourcesChart.tsx | 17 +- .../WorkspaceTiming/ScriptsChart.tsx | 12 +- .../WorkspaceTiming/StagesChart.tsx | 256 ++++++++++-------- .../WorkspaceTimings.stories.tsx | 10 +- .../WorkspaceTiming/WorkspaceTimings.tsx | 128 +++++---- .../WorkspaceTiming/storybookData.ts | 25 ++ site/src/pages/WorkspacePage/Workspace.tsx | 3 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 5 +- 22 files changed, 748 insertions(+), 305 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 48b550c9ed010..a8719397a1559 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9020,6 +9020,28 @@ const docTemplate = `{ } } }, + "codersdk.AgentConnectionTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "stage": { + "$ref": "#/definitions/codersdk.TimingStage" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" + } + } + }, "codersdk.AgentScriptTiming": { "type": "object", "properties": { @@ -9034,7 +9056,7 @@ const docTemplate = `{ "type": "integer" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -9042,6 +9064,12 @@ const docTemplate = `{ }, "status": { "type": "string" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" } } }, @@ -12170,7 +12198,7 @@ const docTemplate = `{ "type": "string" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -13473,6 +13501,29 @@ const docTemplate = `{ "TemplateVersionWarningUnsupportedWorkspaces" ] }, + "codersdk.TimingStage": { + "type": "string", + "enum": [ + "init", + "plan", + "graph", + "apply", + "start", + "stop", + "cron", + "connect" + ], + "x-enum-varnames": [ + "TimingStageInit", + "TimingStagePlan", + "TimingStageGraph", + "TimingStageApply", + "TimingStageStart", + "TimingStageStop", + "TimingStageCron", + "TimingStageConnect" + ] + }, "codersdk.TokenConfig": { "type": "object", "properties": { @@ -14806,7 +14857,14 @@ const docTemplate = `{ "codersdk.WorkspaceBuildTimings": { "type": "object", "properties": { + "agent_connection_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentConnectionTiming" + } + }, "agent_script_timings": { + "description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version", "type": "array", "items": { "$ref": "#/definitions/codersdk.AgentScriptTiming" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c9c79b443d3d0..88bf71bf05758 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7992,6 +7992,28 @@ } } }, + "codersdk.AgentConnectionTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "stage": { + "$ref": "#/definitions/codersdk.TimingStage" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" + } + } + }, "codersdk.AgentScriptTiming": { "type": "object", "properties": { @@ -8006,7 +8028,7 @@ "type": "integer" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -8014,6 +8036,12 @@ }, "status": { "type": "string" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" } } }, @@ -10986,7 +11014,7 @@ "type": "string" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -12228,6 +12256,29 @@ "enum": ["UNSUPPORTED_WORKSPACES"], "x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"] }, + "codersdk.TimingStage": { + "type": "string", + "enum": [ + "init", + "plan", + "graph", + "apply", + "start", + "stop", + "cron", + "connect" + ], + "x-enum-varnames": [ + "TimingStageInit", + "TimingStagePlan", + "TimingStageGraph", + "TimingStageApply", + "TimingStageStart", + "TimingStageStop", + "TimingStageCron", + "TimingStageConnect" + ] + }, "codersdk.TokenConfig": { "type": "object", "properties": { @@ -13475,7 +13526,14 @@ "codersdk.WorkspaceBuildTimings": { "type": "object", "properties": { + "agent_connection_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentConnectionTiming" + } + }, "agent_script_timings": { + "description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version", "type": "array", "items": { "$ref": "#/definitions/codersdk.AgentScriptTiming" diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 774389d46b9b3..b610efe0349f5 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2894,13 +2894,15 @@ func (s *MethodTestSuite) TestSystemFunctions() { }) rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ { - StartedAt: timing.StartedAt, - EndedAt: timing.EndedAt, - Stage: timing.Stage, - ScriptID: timing.ScriptID, - ExitCode: timing.ExitCode, - Status: timing.Status, - DisplayName: script.DisplayName, + StartedAt: timing.StartedAt, + EndedAt: timing.EndedAt, + Stage: timing.Stage, + ScriptID: timing.ScriptID, + ExitCode: timing.ExitCode, + Status: timing.Status, + DisplayName: script.DisplayName, + WorkspaceAgentID: agent.ID, + WorkspaceAgentName: agent.Name, }, } check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 67d4373cb7077..be17abe8dd63b 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5899,15 +5899,31 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex break } } + if script.ID == uuid.Nil { + return nil, xerrors.Errorf("script with ID %s not found", t.ScriptID) + } + + var agent database.WorkspaceAgent + for _, a := range agents { + if a.ID == script.WorkspaceAgentID { + agent = a + break + } + } + if agent.ID == uuid.Nil { + return nil, xerrors.Errorf("agent with ID %s not found", t.ScriptID) + } rows = append(rows, database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ - ScriptID: t.ScriptID, - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: t.Stage, - Status: t.Status, - DisplayName: script.DisplayName, + ScriptID: t.ScriptID, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: t.Stage, + Status: t.Status, + DisplayName: script.DisplayName, + WorkspaceAgentID: agent.ID, + WorkspaceAgentName: agent.Name, }) } return rows, nil diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b983d0e1bcd9d..7d3e97166aa8c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11454,7 +11454,11 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks } const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many -SELECT workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, workspace_agent_scripts.display_name +SELECT + workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, + workspace_agent_scripts.display_name, + workspace_agents.id as workspace_agent_id, + workspace_agents.name as workspace_agent_name FROM workspace_agent_script_timings INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id @@ -11464,13 +11468,15 @@ WHERE workspace_builds.id = $1 ` type GetWorkspaceAgentScriptTimingsByBuildIDRow struct { - ScriptID uuid.UUID `db:"script_id" json:"script_id"` - StartedAt time.Time `db:"started_at" json:"started_at"` - EndedAt time.Time `db:"ended_at" json:"ended_at"` - ExitCode int32 `db:"exit_code" json:"exit_code"` - Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"` - Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` - DisplayName string `db:"display_name" json:"display_name"` + ScriptID uuid.UUID `db:"script_id" json:"script_id"` + StartedAt time.Time `db:"started_at" json:"started_at"` + EndedAt time.Time `db:"ended_at" json:"ended_at"` + ExitCode int32 `db:"exit_code" json:"exit_code"` + Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"` + Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` + DisplayName string `db:"display_name" json:"display_name"` + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + WorkspaceAgentName string `db:"workspace_agent_name" json:"workspace_agent_name"` } func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) { @@ -11490,6 +11496,8 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context &i.Stage, &i.Status, &i.DisplayName, + &i.WorkspaceAgentID, + &i.WorkspaceAgentName, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 2c26740db1d88..df7c829861cb2 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -303,7 +303,11 @@ VALUES RETURNING workspace_agent_script_timings.*; -- name: GetWorkspaceAgentScriptTimingsByBuildID :many -SELECT workspace_agent_script_timings.*, workspace_agent_scripts.display_name +SELECT + workspace_agent_script_timings.*, + workspace_agent_scripts.display_name, + workspace_agents.id as workspace_agent_id, + workspace_agents.name as workspace_agent_name FROM workspace_agent_script_timings INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 0974d85b54d6c..44c34d8a25da3 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -957,15 +957,29 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agent script timings: %w", err) } + resources, err := api.Database.GetWorkspaceResourcesByJobID(ctx, build.JobID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace resources: %w", err) + } + resourceIDs := make([]uuid.UUID, 0, len(resources)) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agents: %w", err) + } + res := codersdk.WorkspaceBuildTimings{ - ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)), - AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)), + ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)), + AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)), + AgentConnectionTimings: make([]codersdk.AgentConnectionTiming, 0, len(agents)), } for _, t := range provisionerTimings { res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{ JobID: t.JobID, - Stage: string(t.Stage), + Stage: codersdk.TimingStage(t.Stage), Source: t.Source, Action: t.Action, Resource: t.Resource, @@ -975,12 +989,23 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) } for _, t := range agentScriptTimings { res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{ - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: string(t.Stage), - Status: string(t.Status), - DisplayName: t.DisplayName, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: codersdk.TimingStage(t.Stage), + Status: string(t.Status), + DisplayName: t.DisplayName, + WorkspaceAgentID: t.WorkspaceAgentID.String(), + WorkspaceAgentName: t.WorkspaceAgentName, + }) + } + for _, agent := range agents { + res.AgentConnectionTimings = append(res.AgentConnectionTimings, codersdk.AgentConnectionTiming{ + WorkspaceAgentID: agent.ID.String(), + WorkspaceAgentName: agent.Name, + StartedAt: agent.CreatedAt, + Stage: codersdk.TimingStageConnect, + EndedAt: agent.FirstConnectedAt.Time, }) } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index e8eeca0f49d66..3aae3989df5b4 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1183,8 +1183,9 @@ func TestPostWorkspaceBuild(t *testing.T) { }) } -//nolint:paralleltest func TestWorkspaceBuildTimings(t *testing.T) { + t.Parallel() + // Setup the test environment with a template and version db, pubsub := dbtestutil.NewDB(t) client := coderdtest.New(t, &coderdtest.Options{ @@ -1237,10 +1238,13 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) } - //nolint:paralleltest t.Run("NonExistentBuild", func(t *testing.T) { - // When: fetching an inexistent build + t.Parallel() + + // Given: a non-existent build buildID := uuid.New() + + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) _, err := client.WorkspaceBuildTimings(ctx, buildID) @@ -1250,10 +1254,13 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Contains(t, err.Error(), "not found") }) - //nolint:paralleltest t.Run("EmptyTimings", func(t *testing.T) { - // When: fetching timings for a build with no timings + t.Parallel() + + // Given: a build with no timings build := makeBuild() + + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) @@ -1264,25 +1271,27 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Empty(t, res.AgentScriptTimings) }) - //nolint:paralleltest t.Run("ProvisionerTimings", func(t *testing.T) { - // When: fetching timings for a build with provisioner timings + t.Parallel() + + // Given: a build with provisioner timings build := makeBuild() provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5) - // Then: return a response with the expected timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) - require.Len(t, res.ProvisionerTimings, 5) + // Then: return a response with the expected timings + require.Len(t, res.ProvisionerTimings, 5) for i := range res.ProvisionerTimings { timingRes := res.ProvisionerTimings[i] genTiming := provisionerTimings[i] require.Equal(t, genTiming.Resource, timingRes.Resource) require.Equal(t, genTiming.Action, timingRes.Action) - require.Equal(t, string(genTiming.Stage), timingRes.Stage) + require.Equal(t, string(genTiming.Stage), string(timingRes.Stage)) require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String()) require.Equal(t, genTiming.Source, timingRes.Source) require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) @@ -1290,9 +1299,10 @@ func TestWorkspaceBuildTimings(t *testing.T) { } }) - //nolint:paralleltest t.Run("AgentScriptTimings", func(t *testing.T) { - // When: fetching timings for a build with agent script timings + t.Parallel() + + // Given: a build with agent script timings build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, @@ -1305,27 +1315,31 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5) - // Then: return a response with the expected timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) - require.Len(t, res.AgentScriptTimings, 5) + // Then: return a response with the expected timings + require.Len(t, res.AgentScriptTimings, 5) for i := range res.AgentScriptTimings { timingRes := res.AgentScriptTimings[i] genTiming := agentScriptTimings[i] require.Equal(t, genTiming.ExitCode, timingRes.ExitCode) require.Equal(t, string(genTiming.Status), timingRes.Status) - require.Equal(t, string(genTiming.Stage), timingRes.Stage) + require.Equal(t, string(genTiming.Stage), string(timingRes.Stage)) require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli()) + require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID) + require.Equal(t, agent.Name, timingRes.WorkspaceAgentName) } }) - //nolint:paralleltest t.Run("NoAgentScripts", func(t *testing.T) { - // When: fetching timings for a build with no agent scripts + t.Parallel() + + // Given: a build with no agent scripts build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, @@ -1334,29 +1348,88 @@ func TestWorkspaceBuildTimings(t *testing.T) { ResourceID: resource.ID, }) - // Then: return a response with empty agent script timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) + + // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) }) // Some workspaces might not have agents. It is improbable, but possible. - //nolint:paralleltest t.Run("NoAgents", func(t *testing.T) { - // When: fetching timings for a build with no agents + t.Parallel() + + // Given: a build with no agents build := makeBuild() dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, }) - // Then: return a response with empty agent script timings - // trigger build + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) + + // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) + require.Empty(t, res.AgentConnectionTimings) + }) + + t.Run("AgentConnectionTimings", func(t *testing.T) { + t.Parallel() + + // Given: a build with an agent + build := makeBuild() + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + + // When: fetching timings for the build + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) + require.NoError(t, err) + + // Then: return a response with the expected timings + require.Len(t, res.AgentConnectionTimings, 1) + for i := range res.ProvisionerTimings { + timingRes := res.AgentConnectionTimings[i] + require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID) + require.Equal(t, agent.Name, timingRes.WorkspaceAgentName) + require.NotEmpty(t, timingRes.StartedAt) + require.NotEmpty(t, timingRes.EndedAt) + } + }) + + t.Run("MultipleAgents", func(t *testing.T) { + t.Parallel() + + // Given: a build with multiple agents + build := makeBuild() + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agents := make([]database.WorkspaceAgent, 5) + for i := range agents { + agents[i] = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + } + + // When: fetching timings for the build + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) + require.NoError(t, err) + + // Then: return a response with the expected timings + require.Len(t, res.AgentConnectionTimings, 5) }) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 3cb00c313f4bf..761be48a9e488 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -175,28 +175,57 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) return params, json.NewDecoder(res.Body).Decode(¶ms) } +type TimingStage string + +const ( + // Based on ProvisionerJobTimingStage + TimingStageInit TimingStage = "init" + TimingStagePlan TimingStage = "plan" + TimingStageGraph TimingStage = "graph" + TimingStageApply TimingStage = "apply" + // Based on WorkspaceAgentScriptTimingStage + TimingStageStart TimingStage = "start" + TimingStageStop TimingStage = "stop" + TimingStageCron TimingStage = "cron" + // Custom timing stage to represent the time taken to connect to an agent + TimingStageConnect TimingStage = "connect" +) + type ProvisionerTiming struct { - JobID uuid.UUID `json:"job_id" format:"uuid"` - StartedAt time.Time `json:"started_at" format:"date-time"` - EndedAt time.Time `json:"ended_at" format:"date-time"` - Stage string `json:"stage"` - Source string `json:"source"` - Action string `json:"action"` - Resource string `json:"resource"` + JobID uuid.UUID `json:"job_id" format:"uuid"` + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + Stage TimingStage `json:"stage"` + Source string `json:"source"` + Action string `json:"action"` + Resource string `json:"resource"` } type AgentScriptTiming struct { - StartedAt time.Time `json:"started_at" format:"date-time"` - EndedAt time.Time `json:"ended_at" format:"date-time"` - ExitCode int32 `json:"exit_code"` - Stage string `json:"stage"` - Status string `json:"status"` - DisplayName string `json:"display_name"` + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + ExitCode int32 `json:"exit_code"` + Stage TimingStage `json:"stage"` + Status string `json:"status"` + DisplayName string `json:"display_name"` + WorkspaceAgentID string `json:"workspace_agent_id"` + WorkspaceAgentName string `json:"workspace_agent_name"` +} + +type AgentConnectionTiming struct { + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + Stage TimingStage `json:"stage"` + WorkspaceAgentID string `json:"workspace_agent_id"` + WorkspaceAgentName string `json:"workspace_agent_name"` } type WorkspaceBuildTimings struct { ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` - AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` + // TODO: Consolidate agent-related timing metrics into a single struct when + // updating the API version + AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` + AgentConnectionTimings []AgentConnectionTiming `json:"agent_connection_timings"` } func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (WorkspaceBuildTimings, error) { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index d49ab50fbb1ef..fc7ecdaa5d570 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1016,14 +1016,25 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ @@ -1033,7 +1044,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ] diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 384933e5795af..c7c1a729476c8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -349,6 +349,28 @@ | --------- | ------ | -------- | ------------ | ----------- | | `license` | string | true | | | +## codersdk.AgentConnectionTiming + +```json +{ + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------------- | -------------------------------------------- | -------- | ------------ | ----------- | +| `ended_at` | string | false | | | +| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | | +| `started_at` | string | false | | | +| `workspace_agent_id` | string | false | | | +| `workspace_agent_name` | string | false | | | + ## codersdk.AgentScriptTiming ```json @@ -356,22 +378,26 @@ "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------- | ------- | -------- | ------------ | ----------- | -| `display_name` | string | false | | | -| `ended_at` | string | false | | | -| `exit_code` | integer | false | | | -| `stage` | string | false | | | -| `started_at` | string | false | | | -| `status` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | -------------------------------------------- | -------- | ------------ | ----------- | +| `display_name` | string | false | | | +| `ended_at` | string | false | | | +| `exit_code` | integer | false | | | +| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | | +| `started_at` | string | false | | | +| `status` | string | false | | | +| `workspace_agent_id` | string | false | | | +| `workspace_agent_name` | string | false | | | ## codersdk.AgentSubsystem @@ -4359,22 +4385,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------ | ------ | -------- | ------------ | ----------- | -| `action` | string | false | | | -| `ended_at` | string | false | | | -| `job_id` | string | false | | | -| `resource` | string | false | | | -| `source` | string | false | | | -| `stage` | string | false | | | -| `started_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------ | -------------------------------------------- | -------- | ------------ | ----------- | +| `action` | string | false | | | +| `ended_at` | string | false | | | +| `job_id` | string | false | | | +| `resource` | string | false | | | +| `source` | string | false | | | +| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | | +| `started_at` | string | false | | | ## codersdk.ProxyHealthReport @@ -5717,6 +5743,27 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | ------------------------ | | `UNSUPPORTED_WORKSPACES` | +## codersdk.TimingStage + +```json +"init" +``` + +### Properties + +#### Enumerated Values + +| Value | +| --------- | +| `init` | +| `plan` | +| `graph` | +| `apply` | +| `start` | +| `stop` | +| `cron` | +| `connect` | + ## codersdk.TokenConfig ```json @@ -7381,14 +7428,25 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ @@ -7398,7 +7456,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ] @@ -7407,10 +7465,11 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | -| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | | -| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------------------- | ------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------- | +| `agent_connection_timings` | array of [codersdk.AgentConnectionTiming](#codersdkagentconnectiontiming) | false | | | +| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | Agent script timings Consolidate agent-related timing metrics into a single struct when updating the API version | +| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | ## codersdk.WorkspaceConnectionLatencyMS diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 283dab5db91b5..183a59ddd13a3 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1641,14 +1641,25 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ @@ -1658,7 +1669,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ] diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 0e8981ba71ea4..45f7ac3bb7fe6 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -57,9 +57,18 @@ export const infiniteWorkspaceBuilds = ( }; }; -export const workspaceBuildTimings = (workspaceBuildId: string) => { +// We use readyAgentsCount to invalidate the query when an agent connects +export const workspaceBuildTimings = ( + workspaceBuildId: string, + readyAgentsCount: number, +) => { return { - queryKey: ["workspaceBuilds", workspaceBuildId, "timings"], + queryKey: [ + "workspaceBuilds", + workspaceBuildId, + "timings", + { readyAgentsCount }, + ], queryFn: () => API.workspaceBuildTimings(workspaceBuildId), }; }; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 8bb28637e526a..619961c457b36 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -32,14 +32,25 @@ export interface AddLicenseRequest { readonly license: string; } +// From codersdk/workspacebuilds.go +export interface AgentConnectionTiming { + readonly started_at: string; + readonly ended_at: string; + readonly stage: TimingStage; + readonly workspace_agent_id: string; + readonly workspace_agent_name: string; +} + // From codersdk/workspacebuilds.go export interface AgentScriptTiming { readonly started_at: string; readonly ended_at: string; readonly exit_code: number; - readonly stage: string; + readonly stage: TimingStage; readonly status: string; readonly display_name: string; + readonly workspace_agent_id: string; + readonly workspace_agent_name: string; } // From codersdk/templates.go @@ -1104,7 +1115,7 @@ export interface ProvisionerTiming { readonly job_id: string; readonly started_at: string; readonly ended_at: string; - readonly stage: string; + readonly stage: TimingStage; readonly source: string; readonly action: string; readonly resource: string; @@ -1986,6 +1997,7 @@ export interface WorkspaceBuildParameter { export interface WorkspaceBuildTimings { readonly provisioner_timings: Readonly>; readonly agent_script_timings: Readonly>; + readonly agent_connection_timings: Readonly>; } // From codersdk/workspaces.go @@ -2226,6 +2238,10 @@ export const TemplateRoles: TemplateRole[] = ["", "admin", "use"] export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES" export const TemplateVersionWarnings: TemplateVersionWarning[] = ["UNSUPPORTED_WORKSPACES"] +// From codersdk/workspacebuilds.go +export type TimingStage = "apply" | "connect" | "cron" | "graph" | "init" | "plan" | "start" | "stop" +export const TimingStages: TimingStage[] = ["apply", "connect", "cron", "graph", "init", "plan", "start", "stop"] + // From codersdk/workspaces.go export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode" export const UsageAppNames: UsageAppName[] = ["jetbrains", "reconnecting-pty", "ssh", "vscode"] diff --git a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx index 3f1f7d761e748..b1c0bd89bc5fe 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx @@ -1,8 +1,5 @@ -import { css } from "@emotion/css"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; +import { type Theme, useTheme } from "@emotion/react"; import { type FC, useState } from "react"; -import { Link } from "react-router-dom"; import { Bar } from "./Chart/Bar"; import { Chart, @@ -30,7 +27,7 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import type { StageCategory } from "./StagesChart"; +import type { Stage } from "./StagesChart"; type ResourceTiming = { name: string; @@ -40,14 +37,12 @@ type ResourceTiming = { }; export type ResourcesChartProps = { - category: StageCategory; - stage: string; + stage: Stage; timings: ResourceTiming[]; onBack: () => void; }; export const ResourcesChart: FC = ({ - category, stage, timings, onBack, @@ -71,11 +66,11 @@ export const ResourcesChart: FC = ({ @@ -89,7 +84,7 @@ export const ResourcesChart: FC = ({ - {stage} stage + {stage.name} stage {visibleTimings.map((t) => ( diff --git a/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx index 64d97bff7cfdb..3824913a87b43 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx @@ -27,7 +27,7 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import type { StageCategory } from "./StagesChart"; +import type { Stage } from "./StagesChart"; type ScriptTiming = { name: string; @@ -37,14 +37,12 @@ type ScriptTiming = { }; export type ScriptsChartProps = { - category: StageCategory; - stage: string; + stage: Stage; timings: ScriptTiming[]; onBack: () => void; }; export const ScriptsChart: FC = ({ - category, stage, timings, onBack, @@ -66,11 +64,11 @@ export const ScriptsChart: FC = ({ @@ -84,7 +82,7 @@ export const ScriptsChart: FC = ({ - {stage} stage + {stage.name} stage {visibleTimings.map((t) => ( diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index dc5550dcfed98..cfb4285f77eda 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -1,6 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import ErrorSharp from "@mui/icons-material/ErrorSharp"; import InfoOutlined from "@mui/icons-material/InfoOutlined"; +import type { TimingStage } from "api/typesGenerated"; import type { FC } from "react"; import { Bar, ClickableBar } from "./Chart/Bar"; import { Blocks } from "./Chart/Blocks"; @@ -28,118 +29,34 @@ import { mergeTimeRanges, } from "./Chart/utils"; -export type StageCategory = { - name: string; - id: "provisioning" | "workspaceBoot"; -}; - -const stageCategories: StageCategory[] = [ - { - name: "provisioning", - id: "provisioning", - }, - { - name: "workspace boot", - id: "workspaceBoot", - }, -] as const; - export type Stage = { - name: string; - categoryID: StageCategory["id"]; + /** + * The name is used to identify the stage. + */ + name: TimingStage; + /** + * The value to display in the stage label. This can differ from the stage + * name to provide more context or clarity. + */ + label: string; + /** + * The section is used to group stages together. + */ + section: string; + /** + * The tooltip is used to provide additional information about the stage. + */ tooltip: Omit; }; -export const stages: Stage[] = [ - { - name: "init", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform initialization - - Download providers & modules. - - - ), - }, - }, - { - name: "plan", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform plan - - Compare state of desired vs actual resources and compute changes to - be made. - - - ), - }, - }, - { - name: "graph", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform graph - - List all resources in plan, used to update coderd database. - - - ), - }, - }, - { - name: "apply", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform apply - - Execute Terraform plan to create/modify/delete resources into - desired states. - - - ), - }, - }, - { - name: "start", - categoryID: "workspaceBoot", - tooltip: { - title: ( - <> - Start - - Scripts executed when the agent is starting. - - - ), - }, - }, -]; - type StageTiming = { - name: string; - /** + stage: Stage; /** * Represents the number of resources included in this stage that can be * inspected. This value is used to display individual blocks within the bar, * indicating that the stage consists of multiple resource time blocks. */ visibleResources: number; - /** - * Represents the category of the stage. This value is used to group stages - * together in the chart. For example, all provisioning stages are grouped - * together. - */ - categoryID: StageCategory["id"]; /** * Represents the time range of the stage. This value is used to calculate the * duration of the stage and to position the stage within the chart. This can @@ -155,7 +72,7 @@ type StageTiming = { export type StagesChartProps = { timings: StageTiming[]; - onSelectStage: (timing: StageTiming, category: StageCategory) => void; + onSelectStage: (stage: Stage) => void; }; export const StagesChart: FC = ({ @@ -167,27 +84,28 @@ export const StagesChart: FC = ({ ); const totalTime = calcDuration(totalRange); const [ticks, scale] = makeTicks(totalTime); + const sections = Array.from(new Set(timings.map((t) => t.stage.section))); return ( - {stageCategories.map((c) => { - const stagesInCategory = stages.filter( - (s) => s.categoryID === c.id, - ); + {sections.map((section) => { + const stages = timings + .filter((t) => t.stage.section === section) + .map((t) => t.stage); return ( - - {c.name} + + {section} - {stagesInCategory.map((stage) => ( + {stages.map((stage) => ( - {stage.name} + {stage.label} @@ -201,19 +119,19 @@ export const StagesChart: FC = ({ - {stageCategories.map((category) => { + {sections.map((section) => { const stageTimings = timings.filter( - (t) => t.categoryID === category.id, + (t) => t.stage.section === section, ); return ( - + {stageTimings.map((t) => { // If the stage has no timing data, we just want to render an empty row if (t.range === undefined) { return ( ); } @@ -223,18 +141,18 @@ export const StagesChart: FC = ({ return ( {/** We only want to expand stages with more than one resource */} {t.visibleResources > 1 ? ( { - onSelectStage(t, category); + onSelectStage(t.stage); }} > {t.error && ( @@ -281,3 +199,103 @@ const styles = { cursor: "pointer", }), } satisfies Record>; + +export const provisioningStages: Stage[] = [ + { + name: "init", + label: "init", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform initialization + + Download providers & modules. + + + ), + }, + }, + { + name: "plan", + label: "plan", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform plan + + Compare state of desired vs actual resources and compute changes to + be made. + + + ), + }, + }, + { + name: "graph", + label: "graph", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform graph + + List all resources in plan, used to update coderd database. + + + ), + }, + }, + { + name: "apply", + label: "apply", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform apply + + Execute Terraform plan to create/modify/delete resources into + desired states. + + + ), + }, + }, +]; + +export const agentStages = (section: string): Stage[] => { + return [ + { + name: "connect", + label: "connect", + section, + tooltip: { + title: ( + <> + Connect + + Establish an RPC connection with the control plane. + + + ), + }, + }, + { + name: "start", + label: "run startup scripts", + section, + tooltip: { + title: ( + <> + Run startup scripts + + Execute each agent startup script. + + + ), + }, + }, + ]; +}; diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index f546e271395ab..5e3ccb86151d0 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -11,6 +11,7 @@ const meta: Meta = { defaultIsOpen: true, provisionerTimings: WorkspaceTimingsResponse.provisioner_timings, agentScriptTimings: WorkspaceTimingsResponse.agent_script_timings, + agentConnectionTimings: WorkspaceTimingsResponse.agent_connection_timings, }, parameters: { chromatic, @@ -32,6 +33,7 @@ export const Loading: Story = { args: { provisionerTimings: undefined, agentScriptTimings: undefined, + agentConnectionTimings: undefined, }, }; @@ -45,7 +47,7 @@ export const ClickToOpen: Story = { play: async ({ canvasElement }) => { const user = userEvent.setup(); const canvas = within(canvasElement); - await user.click(canvas.getByRole("button")); + await user.click(canvas.getByText("Build timeline", { exact: false })); await canvas.findByText("provisioning"); }, }; @@ -58,9 +60,9 @@ export const ClickToClose: Story = { const user = userEvent.setup(); const canvas = within(canvasElement); await canvas.findByText("provisioning"); - await user.click(canvas.getByText("Provisioning time", { exact: false })); + await user.click(canvas.getByText("Build timeline", { exact: false })); await waitFor(() => - expect(canvas.getByText("workspace boot")).not.toBeVisible(), + expect(canvas.queryByText("workspace boot")).not.toBeInTheDocument(), ); }, }; @@ -96,7 +98,7 @@ export const NavigateToStartStage: Story = { const user = userEvent.setup(); const canvas = within(canvasElement); const detailsButton = canvas.getByRole("button", { - name: "View start details", + name: "View run startup scripts details", }); await user.click(detailsButton); await canvas.findByText("Startup Script"); diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index 9e16e55bae36e..9fe12b122a35c 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -4,19 +4,27 @@ import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp"; import Button from "@mui/material/Button"; import Collapse from "@mui/material/Collapse"; import Skeleton from "@mui/material/Skeleton"; -import type { AgentScriptTiming, ProvisionerTiming } from "api/typesGenerated"; +import type { + AgentConnectionTiming, + AgentScriptTiming, + ProvisionerTiming, +} from "api/typesGenerated"; import { type FC, useState } from "react"; import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils"; import { ResourcesChart, isCoderResource } from "./ResourcesChart"; import { ScriptsChart } from "./ScriptsChart"; -import { type StageCategory, StagesChart, stages } from "./StagesChart"; +import { + type Stage, + StagesChart, + agentStages, + provisioningStages, +} from "./StagesChart"; type TimingView = | { name: "default" } | { name: "detailed"; - stage: string; - category: StageCategory; + stage: Stage; filter: string; }; @@ -24,20 +32,37 @@ type WorkspaceTimingsProps = { defaultIsOpen?: boolean; provisionerTimings: readonly ProvisionerTiming[] | undefined; agentScriptTimings: readonly AgentScriptTiming[] | undefined; + agentConnectionTimings: readonly AgentConnectionTiming[] | undefined; }; export const WorkspaceTimings: FC = ({ provisionerTimings = [], agentScriptTimings = [], + agentConnectionTimings = [], defaultIsOpen = false, }) => { const [view, setView] = useState({ name: "default" }); - const timings = [...provisionerTimings, ...agentScriptTimings]; + const timings = [ + ...provisionerTimings, + ...agentScriptTimings, + ...agentConnectionTimings, + ]; const [isOpen, setIsOpen] = useState(defaultIsOpen); const isLoading = timings.length === 0; + // All stages + const agentStageLabels = Array.from( + new Set( + agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`), + ), + ); + const stages = [ + ...provisioningStages, + ...agentStageLabels.flatMap((a) => agentStages(a)), + ]; + const displayProvisioningTime = () => { - const totalRange = mergeTimeRanges(timings.map(extractRange)); + const totalRange = mergeTimeRanges(timings.map(toTimeRange)); const totalDuration = calcDuration(totalRange); return humanizeDuration(totalDuration); }; @@ -81,7 +106,7 @@ export const WorkspaceTimings: FC = ({ const stageRange = stageTimings.length === 0 ? undefined - : mergeTimeRanges(stageTimings.map(extractRange)); + : mergeTimeRanges(stageTimings.map(toTimeRange)); // Prevent users from inspecting internal coder resources in // provisioner timings. @@ -93,67 +118,63 @@ export const WorkspaceTimings: FC = ({ }); return { + stage: s, range: stageRange, - name: s.name, - categoryID: s.categoryID, visibleResources: visibleResources.length, error: stageTimings.some( (t) => "status" in t && t.status === "exit_failure", ), }; })} - onSelectStage={(t, category) => { + onSelectStage={(stage) => { setView({ + stage, name: "detailed", - stage: t.name, - category, filter: "", }); }} /> )} - {view.name === "detailed" && - view.category.id === "provisioning" && ( - t.stage === view.stage) - .map((t) => { - return { - range: extractRange(t), + {view.name === "detailed" && ( + <> + {view.stage.section === "provisioning" && ( + t.stage === view.stage.name) + .map((t) => ({ + range: toTimeRange(t), name: t.resource, source: t.source, action: t.action, - }; - })} - category={view.category} - stage={view.stage} - onBack={() => { - setView({ name: "default" }); - }} - /> - )} - - {view.name === "detailed" && - view.category.id === "workspaceBoot" && ( - t.stage === view.stage) - .map((t) => { - return { - range: extractRange(t), - name: t.display_name, - status: t.status, - exitCode: t.exit_code, - }; - })} - category={view.category} - stage={view.stage} - onBack={() => { - setView({ name: "default" }); - }} - /> - )} + }))} + stage={view.stage} + onBack={() => { + setView({ name: "default" }); + }} + /> + )} + + {view.stage.name === "start" && ( + t.stage === view.stage.name) + .map((t) => { + return { + range: toTimeRange(t), + name: t.display_name, + status: t.status, + exitCode: t.exit_code, + }; + })} + stage={view.stage} + onBack={() => { + setView({ name: "default" }); + }} + /> + )} + + )} )} @@ -161,9 +182,10 @@ export const WorkspaceTimings: FC = ({ ); }; -const extractRange = ( - timing: ProvisionerTiming | AgentScriptTiming, -): TimeRange => { +const toTimeRange = (timing: { + started_at: string; + ended_at: string; +}): TimeRange => { return { startedAt: new Date(timing.started_at), endedAt: new Date(timing.ended_at), diff --git a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts index 828959f424107..589d95e6153da 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts @@ -355,6 +355,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Startup Script", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650915Z", @@ -363,6 +365,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Dotfiles", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650715Z", @@ -371,6 +375,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Personalize", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650512Z", @@ -379,6 +385,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "install_slackme", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650659Z", @@ -387,6 +395,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Coder Login", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650666Z", @@ -395,6 +405,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "File Browser", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.652425Z", @@ -403,6 +415,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "code-server", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650423Z", @@ -411,6 +425,17 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Git Clone", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", + }, + ], + agent_connection_timings: [ + { + started_at: "2024-10-14T11:30:55.650423Z", + ended_at: "2024-10-14T11:30:56.650423Z", + stage: "connect", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, ], }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index c54ab25c1006c..5b9919474a620 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -267,8 +267,9 @@ export const Workspace: FC = ({ )} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 6859a5ada7882..cdb47f86f508c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -158,8 +158,11 @@ export const WorkspaceReadyPage: FC = ({ const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); // Build Timings. Fetch build timings only when the build job is completed. + const readyAgents = workspace.latest_build.resources + .flatMap((r) => r.agents) + .filter((a) => a && a.lifecycle_state !== "starting"); const timingsQuery = useQuery({ - ...workspaceBuildTimings(workspace.latest_build.id), + ...workspaceBuildTimings(workspace.latest_build.id, readyAgents.length), enabled: Boolean(workspace.latest_build.job.completed_at), }); From 399c830b513ce857596d59ffd2d0125ad71456ab Mon Sep 17 00:00:00 2001 From: Stephen Kirby <58410745+stirby@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:17:50 -0500 Subject: [PATCH 032/223] chore(docs): add info on new build timings in docs (#15310) Adds information on workspace build timings in workspace lifecycle and template troubleshooting docs. --- docs/admin/templates/troubleshooting.md | 14 ++++++++++++++ .../workspace-build-timings-ui.png | Bin 0 -> 144112 bytes docs/user-guides/workspace-lifecycle.md | 13 +++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 docs/images/admin/templates/troubleshooting/workspace-build-timings-ui.png diff --git a/docs/admin/templates/troubleshooting.md b/docs/admin/templates/troubleshooting.md index 7c61dfaa8be65..e08a422938e2f 100644 --- a/docs/admin/templates/troubleshooting.md +++ b/docs/admin/templates/troubleshooting.md @@ -154,3 +154,17 @@ the top of the script to exit on error. > **Note:** If you aren't seeing any logs, check that the `dir` directive points > to a valid directory in the file system. + +## Slow workspace startup times + +If your workspaces are taking longer to start than expected, or longer than +desired, you can diagnose which steps have the highest impact in the workspace +build timings UI (available in v2.17 and beyond). Admins can can +programmatically pull startup times for individual workspace builds using our +[build timings API endpoint](../../reference/api/builds.md#get-workspace-build-timings-by-id). + +See our +[guide on optimizing workspace build times](../../tutorials/best-practices/speed-up-templates.md) +to optimize your templates based on this data. + +![Workspace build timings UI](../../images/admin/templates/troubleshooting/workspace-build-timings-ui.png) diff --git a/docs/images/admin/templates/troubleshooting/workspace-build-timings-ui.png b/docs/images/admin/templates/troubleshooting/workspace-build-timings-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..137752ec1aa62f0275b0ea4f2d80b0fb609646af GIT binary patch literal 144112 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

=WIu>kUvR2DYN%--t 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

+zI@e` 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

(P35Q7=%kmA+ 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 bP^(Iax)AAb54M=BS#w-n|5(Pic31xeF9l+D literal 0 HcmV?d00001 diff --git a/docs/user-guides/workspace-lifecycle.md b/docs/user-guides/workspace-lifecycle.md index 12c2b021112dc..56d0c0b5ba7fd 100644 --- a/docs/user-guides/workspace-lifecycle.md +++ b/docs/user-guides/workspace-lifecycle.md @@ -109,6 +109,19 @@ your template's Terraform file and the target resources on your infrastructure. Unhealthy workspaces are usually caused by a misconfiguration in the agent or workspace startup scripts. +## Workspace build times + +After a successful build, you can see a timing breakdown of the workspace +startup process from the dashboard (starting in v2.17). We capture and display +both time taken to provision the workspace's compute and agent startup steps. +These include any +[`coder_script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script)s +such as [dotfiles](./workspace-dotfiles.md) or +[`coder_app`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app) +startups. + +![Workspace build timings UI](../images/admin/templates/troubleshooting/workspace-build-timings-ui.png) + ### Next steps - [Connecting to your workspace](./index.md) From 1f93b80e09b943ab96906f4851a1a14a4d56579b Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Fri, 1 Nov 2024 15:43:51 -0400 Subject: [PATCH 033/223] chore: fix docs/admin links and upgrade notice (#15043) - Update links to /docs/admin to match the new structure - TODO: remove the release string from the "upgrade available" instructions link - [x] https://github.com/coder/coder/blob/update-upgrade-config-links/cli/server.go#L646 ![2024-10-11_11-35-40](https://github.com/user-attachments/assets/fd95e821-d5ad-4c91-a38a-066046c7072c) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- cli/server.go | 2 +- cli/templatecreate.go | 2 +- cli/templateedit.go | 2 +- cli/testdata/coder_templates_create_--help.golden | 2 +- cli/testdata/coder_templates_edit_--help.golden | 2 +- docs/admin/templates/extending-templates/web-ides.md | 2 +- docs/reference/cli/templates_create.md | 2 +- docs/reference/cli/templates_edit.md | 2 +- install.sh | 2 +- site/src/api/queries/workspaces.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/server.go b/cli/server.go index c053d8dc7ef02..aa0a010eb0aa4 100644 --- a/cli/server.go +++ b/cli/server.go @@ -813,7 +813,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } defer options.Telemetry.Close() } else { - logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/telemetry`, vals.DocsURL.String())) + logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/setup/telemetry`, vals.DocsURL.String())) } // This prevents the pprof import from being accidentally deleted. diff --git a/cli/templatecreate.go b/cli/templatecreate.go index beef00650847c..c45277bec5837 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -237,7 +237,7 @@ func (r *RootCmd) templateCreate() *serpent.Command { }, { Flag: "require-active-version", - Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/templates/general-settings#require-automatic-updates-enterprise for more details.", + Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details.", Value: serpent.BoolOf(&requireActiveVersion), Default: "false", }, diff --git a/cli/templateedit.go b/cli/templateedit.go index 8d0ecf3e20a76..44d77ff4489b6 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -290,7 +290,7 @@ func (r *RootCmd) templateEdit() *serpent.Command { }, { Flag: "require-active-version", - Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/templates/general-settings#require-automatic-updates-enterprise for more details.", + Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details.", Value: serpent.BoolOf(&requireActiveVersion), Default: "false", }, diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden index 5cbd079355449..80cccb24a57e3 100644 --- a/cli/testdata/coder_templates_create_--help.golden +++ b/cli/testdata/coder_templates_create_--help.golden @@ -55,7 +55,7 @@ OPTIONS: Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See - https://coder.com/docs/templates/general-settings#require-automatic-updates-enterprise + https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details. --var string-array diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden index eab60ac359c66..76dee16cf993c 100644 --- a/cli/testdata/coder_templates_edit_--help.golden +++ b/cli/testdata/coder_templates_edit_--help.golden @@ -87,7 +87,7 @@ OPTIONS: Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See - https://coder.com/docs/templates/general-settings#require-automatic-updates-enterprise + https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details. -y, --yes bool diff --git a/docs/admin/templates/extending-templates/web-ides.md b/docs/admin/templates/extending-templates/web-ides.md index fbfd2bab42220..1ded4fbf3482b 100644 --- a/docs/admin/templates/extending-templates/web-ides.md +++ b/docs/admin/templates/extending-templates/web-ides.md @@ -255,7 +255,7 @@ resource "coder_app" "rstudio" { ``` If you cannot enable a -[wildcard subdomain](https://coder.com/docs/admin/configure#wildcard-access-url), +[wildcard subdomain](https://coder.com/docs/admin/setup#wildcard-access-url), you can configure the template to run RStudio on a path using an NGINX reverse proxy in the template. There is however [security risk](https://coder.com/docs/reference/cli/server#--dangerous-allow-path-app-sharing) diff --git a/docs/reference/cli/templates_create.md b/docs/reference/cli/templates_create.md index 9346948072cc8..01b153ff2911d 100644 --- a/docs/reference/cli/templates_create.md +++ b/docs/reference/cli/templates_create.md @@ -95,7 +95,7 @@ Specify a duration workspaces may be in the dormant state prior to being deleted | Type | bool | | Default | false | -Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/templates/general-settings#require-automatic-updates-enterprise for more details. +Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details. ### -y, --yes diff --git a/docs/reference/cli/templates_edit.md b/docs/reference/cli/templates_edit.md index b9a613bdd8a6a..81fdc04d1a176 100644 --- a/docs/reference/cli/templates_edit.md +++ b/docs/reference/cli/templates_edit.md @@ -153,7 +153,7 @@ Allow users to customize the autostop TTL for workspaces on this template. This | Type | bool | | Default | false | -Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/templates/general-settings#require-automatic-updates-enterprise for more details. +Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details. ### --private diff --git a/install.sh b/install.sh index 40753f2f9973c..e6a553eaac1fb 100755 --- a/install.sh +++ b/install.sh @@ -216,7 +216,7 @@ To run a Coder server: # Or just run the server directly $ coder server - Configuring Coder: https://coder.com/docs/admin/configure + Configuring Coder: https://coder.com/docs/admin/setup To connect to a Coder deployment: diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 87bdc158b8058..ee390e542c42c 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -61,7 +61,7 @@ type AutoCreateWorkspaceOptions = { * If provided, the auto-create workspace feature will attempt to find a * matching workspace. If found, it will return the existing workspace instead * of creating a new one. Its value supports [advanced filtering queries for - * workspaces](https://coder.com/docs/workspaces#workspace-filtering). If + * workspaces](https://coder.com/docs/user-guides/workspace-management#workspace-filtering). If * multiple values are returned, the first one will be returned. */ match: string | null; From 040e5cf9f3f1ffdcca97063dbfb56d2a9705e79b Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Sun, 3 Nov 2024 10:01:06 -0600 Subject: [PATCH 034/223] docs: provide example regexes to properly escape . characters (#14983) --- docs/admin/external-auth.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/admin/external-auth.md b/docs/admin/external-auth.md index 70aade966c499..51f11f53d2754 100644 --- a/docs/admin/external-auth.md +++ b/docs/admin/external-auth.md @@ -191,20 +191,20 @@ CODER_EXTERNAL_AUTH_0_ID=primary-github CODER_EXTERNAL_AUTH_0_TYPE=github CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx -CODER_EXTERNAL_AUTH_0_REGEX=github.com/org +CODER_EXTERNAL_AUTH_0_REGEX=github\.com/org # Provider 2) github.example.com CODER_EXTERNAL_AUTH_1_ID=secondary-github CODER_EXTERNAL_AUTH_1_TYPE=github CODER_EXTERNAL_AUTH_1_CLIENT_ID=xxxxxx CODER_EXTERNAL_AUTH_1_CLIENT_SECRET=xxxxxxx -CODER_EXTERNAL_AUTH_1_REGEX=github.example.com +CODER_EXTERNAL_AUTH_1_REGEX=github\.example\.com CODER_EXTERNAL_AUTH_1_AUTH_URL="https://github.example.com/login/oauth/authorize" CODER_EXTERNAL_AUTH_1_TOKEN_URL="https://github.example.com/login/oauth/access_token" CODER_EXTERNAL_AUTH_1_VALIDATE_URL="https://github.example.com/api/v3/user" ``` -To support regex matching for paths (e.g. github.com/org), you'll need to add +To support regex matching for paths (e.g. github\.com/org), you'll need to add this to the [Coder agent startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script): From 48253581ed7d3b3e171b82458f91988099e1626e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:54:43 +0500 Subject: [PATCH 035/223] chore: bump chromatic from 11.3.0 to 11.16.3 in /site (#15329) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- site/package.json | 2 +- site/pnpm-lock.yaml | 26 ++++++-------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/site/package.json b/site/package.json index d404d75e800d3..04fe6a302b68a 100644 --- a/site/package.json +++ b/site/package.json @@ -141,7 +141,7 @@ "@types/ua-parser-js": "0.7.36", "@types/uuid": "9.0.2", "@vitejs/plugin-react": "4.3.2", - "chromatic": "11.3.0", + "chromatic": "11.16.3", "eventsourcemock": "2.0.0", "express": "4.21.0", "jest": "29.7.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 99f5d8d81ffa4..817ea0e6fcc19 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -332,8 +332,8 @@ importers: specifier: 4.3.2 version: 4.3.2(vite@5.4.8(@types/node@20.16.10)) chromatic: - specifier: 11.3.0 - version: 11.3.0 + specifier: 11.16.3 + version: 11.16.3 eventsourcemock: specifier: 2.0.0 version: 2.0.0 @@ -3021,20 +3021,8 @@ packages: chroma-js@2.4.2: resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==} - chromatic@11.3.0: - resolution: {integrity: sha512-q1ZtJDJrjLGnz60ivpC16gmd7KFzcaA4eTb7gcytCqbaKqlHhCFr1xQmcUDsm14CK7JsqdkFU6S+JQdOd2ZNJg==} - hasBin: true - peerDependencies: - '@chromatic-com/cypress': ^0.*.* || ^1.0.0 - '@chromatic-com/playwright': ^0.*.* || ^1.0.0 - peerDependenciesMeta: - '@chromatic-com/cypress': - optional: true - '@chromatic-com/playwright': - optional: true - - chromatic@11.5.4: - resolution: {integrity: sha512-+J+CopeUSyGUIQJsU6X7CfvSmeVBs0j6LZ9AgF4+XTjI4pFmUiUXsTc00rH9x9W1jCppOaqDXv2kqJJXGDK3mA==} + chromatic@11.16.3: + resolution: {integrity: sha512-bckarRbZ3M1BvsmhLqEMschuQPk2FlSD9cvy8383JwoVvaIqLr0dv1tI/DPM4LMuXOjTjeBSZZINVH9r3RMiiA==} hasBin: true peerDependencies: '@chromatic-com/cypress': ^0.*.* || ^1.0.0 @@ -6458,7 +6446,7 @@ snapshots: '@chromatic-com/storybook@1.9.0(react@18.3.1)': dependencies: - chromatic: 11.5.4 + chromatic: 11.16.3 filesize: 10.1.2 jsonfile: 6.1.0 react-confetti: 6.1.0(react@18.3.1) @@ -9065,9 +9053,7 @@ snapshots: chroma-js@2.4.2: {} - chromatic@11.3.0: {} - - chromatic@11.5.4: {} + chromatic@11.16.3: {} ci-info@3.9.0: {} From 8a72e9a4d2dd3474e12e10872afa9eb9225e380c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:55:14 +0500 Subject: [PATCH 036/223] chore: bump next from 14.2.14 to 14.2.16 in /offlinedocs (#15324) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- offlinedocs/package.json | 2 +- offlinedocs/pnpm-lock.yaml | 90 +++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/offlinedocs/package.json b/offlinedocs/package.json index 29dfc5fb96c95..81b922a358481 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -20,7 +20,7 @@ "framer-motion": "^10.18.0", "front-matter": "4.0.2", "lodash": "4.17.21", - "next": "14.2.14", + "next": "14.2.16", "react": "18.3.1", "react-dom": "18.3.1", "react-icons": "4.12.0", diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index 0b47e84558461..13db7041f4e67 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 4.17.21 version: 4.17.21 next: - specifier: 14.2.14 - version: 14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.16 + version: 14.2.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -275,62 +275,62 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@next/env@14.2.14': - resolution: {integrity: sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg==} + '@next/env@14.2.16': + resolution: {integrity: sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==} '@next/eslint-plugin-next@14.2.16': resolution: {integrity: sha512-noORwKUMkKc96MWjTOwrsUCjky0oFegHbeJ1yEnQBGbMHAaTEIgLZIIfsYF0x3a06PiS+2TXppfifR+O6VWslg==} - '@next/swc-darwin-arm64@14.2.14': - resolution: {integrity: sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==} + '@next/swc-darwin-arm64@14.2.16': + resolution: {integrity: sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.14': - resolution: {integrity: sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww==} + '@next/swc-darwin-x64@14.2.16': + resolution: {integrity: sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.14': - resolution: {integrity: sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg==} + '@next/swc-linux-arm64-gnu@14.2.16': + resolution: {integrity: sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.14': - resolution: {integrity: sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A==} + '@next/swc-linux-arm64-musl@14.2.16': + resolution: {integrity: sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.14': - resolution: {integrity: sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w==} + '@next/swc-linux-x64-gnu@14.2.16': + resolution: {integrity: sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.14': - resolution: {integrity: sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ==} + '@next/swc-linux-x64-musl@14.2.16': + resolution: {integrity: sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.14': - resolution: {integrity: sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A==} + '@next/swc-win32-arm64-msvc@14.2.16': + resolution: {integrity: sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.14': - resolution: {integrity: sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ==} + '@next/swc-win32-ia32-msvc@14.2.16': + resolution: {integrity: sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.14': - resolution: {integrity: sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==} + '@next/swc-win32-x64-msvc@14.2.16': + resolution: {integrity: sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1662,8 +1662,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@14.2.14: - resolution: {integrity: sha512-Q1coZG17MW0Ly5x76shJ4dkC23woLAhhnDnw+DfTc7EpZSGuWrlsZ3bZaO8t6u1Yu8FVfhkqJE+U8GC7E0GLPQ==} + next@14.2.16: + resolution: {integrity: sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -2632,37 +2632,37 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@next/env@14.2.14': {} + '@next/env@14.2.16': {} '@next/eslint-plugin-next@14.2.16': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.14': + '@next/swc-darwin-arm64@14.2.16': optional: true - '@next/swc-darwin-x64@14.2.14': + '@next/swc-darwin-x64@14.2.16': optional: true - '@next/swc-linux-arm64-gnu@14.2.14': + '@next/swc-linux-arm64-gnu@14.2.16': optional: true - '@next/swc-linux-arm64-musl@14.2.14': + '@next/swc-linux-arm64-musl@14.2.16': optional: true - '@next/swc-linux-x64-gnu@14.2.14': + '@next/swc-linux-x64-gnu@14.2.16': optional: true - '@next/swc-linux-x64-musl@14.2.14': + '@next/swc-linux-x64-musl@14.2.16': optional: true - '@next/swc-win32-arm64-msvc@14.2.14': + '@next/swc-win32-arm64-msvc@14.2.16': optional: true - '@next/swc-win32-ia32-msvc@14.2.14': + '@next/swc-win32-ia32-msvc@14.2.16': optional: true - '@next/swc-win32-x64-msvc@14.2.14': + '@next/swc-win32-x64-msvc@14.2.16': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4428,9 +4428,9 @@ snapshots: natural-compare@1.4.0: {} - next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.14 + '@next/env': 14.2.16 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001639 @@ -4440,15 +4440,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.14 - '@next/swc-darwin-x64': 14.2.14 - '@next/swc-linux-arm64-gnu': 14.2.14 - '@next/swc-linux-arm64-musl': 14.2.14 - '@next/swc-linux-x64-gnu': 14.2.14 - '@next/swc-linux-x64-musl': 14.2.14 - '@next/swc-win32-arm64-msvc': 14.2.14 - '@next/swc-win32-ia32-msvc': 14.2.14 - '@next/swc-win32-x64-msvc': 14.2.14 + '@next/swc-darwin-arm64': 14.2.16 + '@next/swc-darwin-x64': 14.2.16 + '@next/swc-linux-arm64-gnu': 14.2.16 + '@next/swc-linux-arm64-musl': 14.2.16 + '@next/swc-linux-x64-gnu': 14.2.16 + '@next/swc-linux-x64-musl': 14.2.16 + '@next/swc-win32-arm64-msvc': 14.2.16 + '@next/swc-win32-ia32-msvc': 14.2.16 + '@next/swc-win32-x64-msvc': 14.2.16 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros From 7b81b3fb8a302d73465472a77ff4a6e1e22c15de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:55:22 +0500 Subject: [PATCH 037/223] chore: bump @types/lodash from 4.14.196 to 4.17.13 in /offlinedocs (#15325) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- offlinedocs/package.json | 2 +- offlinedocs/pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/offlinedocs/package.json b/offlinedocs/package.json index 81b922a358481..c97a0006c4eb6 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -29,7 +29,7 @@ "remark-gfm": "4.0.0" }, "devDependencies": { - "@types/lodash": "4.14.196", + "@types/lodash": "4.17.13", "@types/node": "20.16.10", "@types/react": "18.3.11", "@types/react-dom": "18.3.0", diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index 13db7041f4e67..9a4d91652f804 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -52,8 +52,8 @@ importers: version: 4.0.0 devDependencies: '@types/lodash': - specifier: 4.14.196 - version: 4.14.196 + specifier: 4.17.13 + version: 4.17.13 '@types/node': specifier: 20.16.10 version: 20.16.10 @@ -385,8 +385,8 @@ packages: '@types/lodash.mergewith@4.6.9': resolution: {integrity: sha512-fgkoCAOF47K7sxrQ7Mlud2TH023itugZs2bUg8h/KzT+BnZNrR2jAOmaokbLunHNnobXVWOezAeNn/lZqwxkcw==} - '@types/lodash@4.14.196': - resolution: {integrity: sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==} + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} '@types/mdast@4.0.3': resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} @@ -2718,9 +2718,9 @@ snapshots: '@types/lodash.mergewith@4.6.9': dependencies: - '@types/lodash': 4.14.196 + '@types/lodash': 4.17.13 - '@types/lodash@4.14.196': {} + '@types/lodash@4.17.13': {} '@types/mdast@4.0.3': dependencies: From 9f867a48c3ce76065afc7d80ae3bb8fca183e2fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:00:27 +0500 Subject: [PATCH 038/223] chore: bump typescript from 5.6.2 to 5.6.3 in /site (#15335) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- site/package.json | 2 +- site/pnpm-lock.yaml | 108 ++++++++++++++++++++++---------------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/site/package.json b/site/package.json index 04fe6a302b68a..8b102d662517a 100644 --- a/site/package.json +++ b/site/package.json @@ -161,7 +161,7 @@ "ts-node": "10.9.1", "ts-proto": "1.164.0", "ts-prune": "0.10.3", - "typescript": "5.6.2", + "typescript": "5.6.3", "vite": "5.4.8", "vite-plugin-checker": "0.8.0", "vite-plugin-turbosnap": "1.0.3" diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 817ea0e6fcc19..9c2827f3cf94d 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -234,7 +234,7 @@ importers: version: 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-interactions': specifier: 8.1.11 - version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) '@storybook/addon-links': specifier: 8.1.11 version: 8.1.11(react@18.3.1) @@ -249,13 +249,13 @@ importers: version: 8.1.11 '@storybook/react': specifier: 8.1.11 - version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) + version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@storybook/react-vite': specifier: 8.1.11 - version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10)) + version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) '@storybook/test': specifier: 8.1.11 - version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) '@swc/core': specifier: 1.3.38 version: 1.3.38 @@ -264,7 +264,7 @@ importers: version: 0.2.36(@swc/core@1.3.38) '@testing-library/jest-dom': specifier: 6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) '@testing-library/react': specifier: 14.3.1 version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -342,7 +342,7 @@ importers: version: 4.21.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) jest-canvas-mock: specifier: 2.5.2 version: 2.5.2 @@ -360,7 +360,7 @@ importers: version: 0.1.14(@swc/core@1.3.38)(@swc/jest@0.2.36(@swc/core@1.3.38)) msw: specifier: 2.3.5 - version: 2.3.5(typescript@5.6.2) + version: 2.3.5(typescript@5.6.3) prettier: specifier: 3.3.3 version: 3.3.3 @@ -384,7 +384,7 @@ importers: version: 0.6.0(react-dom@18.3.1(react@18.3.1)) ts-node: specifier: 10.9.1 - version: 10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2) + version: 10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3) ts-proto: specifier: 1.164.0 version: 1.164.0 @@ -392,14 +392,14 @@ importers: specifier: 0.10.3 version: 0.10.3 typescript: - specifier: 5.6.2 - version: 5.6.2 + specifier: 5.6.3 + version: 5.6.3 vite: specifier: 5.4.8 version: 5.4.8(@types/node@20.16.10) vite-plugin-checker: specifier: 0.8.0 - version: 0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10)) + version: 0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 @@ -5738,8 +5738,8 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -6905,7 +6905,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -6919,7 +6919,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -7087,15 +7087,15 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.6.2) + react-docgen-typescript: 2.2.2(typescript@5.6.3) vite: 5.4.8(@types/node@20.16.10) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 '@jridgewell/gen-mapping@0.3.5': dependencies: @@ -7666,11 +7666,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@storybook/addon-interactions@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.11 - '@storybook/test': 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + '@storybook/test': 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) '@storybook/types': 8.1.11 polished: 4.2.2 ts-dedent: 2.2.0 @@ -7791,7 +7791,7 @@ snapshots: - prettier - supports-color - '@storybook/builder-vite@8.1.11(prettier@3.3.3)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10))': + '@storybook/builder-vite@8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10))': dependencies: '@storybook/channels': 8.1.11 '@storybook/client-logger': 8.1.11 @@ -7812,7 +7812,7 @@ snapshots: ts-dedent: 2.2.0 vite: 5.4.8(@types/node@20.16.10) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - encoding - prettier @@ -8037,13 +8037,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-vite@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10))': + '@storybook/react-vite@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) '@rollup/pluginutils': 5.0.5(rollup@4.24.0) - '@storybook/builder-vite': 8.1.11(prettier@3.3.3)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10)) + '@storybook/builder-vite': 8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) '@storybook/node-logger': 8.1.11 - '@storybook/react': 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) + '@storybook/react': 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@storybook/types': 8.1.11 find-up: 5.0.0 magic-string: 0.30.5 @@ -8062,7 +8062,7 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)': + '@storybook/react@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': dependencies: '@storybook/client-logger': 8.1.11 '@storybook/docs-tools': 8.1.11(prettier@3.3.3) @@ -8088,7 +8088,7 @@ snapshots: type-fest: 2.19.0 util-deprecate: 1.0.2 optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - encoding - prettier @@ -8115,14 +8115,14 @@ snapshots: core-js: 3.32.0 find-up: 4.1.0 - '@storybook/test@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@storybook/test@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': dependencies: '@storybook/client-logger': 8.1.11 '@storybook/core-events': 8.1.11 '@storybook/instrumenter': 8.1.11 '@storybook/preview-api': 8.1.11 '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2))) + '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 @@ -8256,7 +8256,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.25.6 @@ -8269,9 +8269,9 @@ snapshots: optionalDependencies: '@jest/globals': 29.7.0 '@types/jest': 29.5.14 - jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)))': + '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.7 @@ -8284,7 +8284,7 @@ snapshots: optionalDependencies: '@jest/globals': 29.7.0 '@types/jest': 29.5.14 - jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) '@testing-library/react-hooks@8.0.1(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -9147,13 +9147,13 @@ snapshots: nan: 2.20.0 optional: true - create-jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)): + create-jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -10280,16 +10280,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)): + jest-cli@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + create-jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -10299,7 +10299,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)): + jest-config@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): dependencies: '@babel/core': 7.25.8 '@jest/test-sequencer': 29.7.0 @@ -10325,7 +10325,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.16.10 - ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2) + ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10608,12 +10608,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)): + jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2)) + jest-cli: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -11177,7 +11177,7 @@ snapshots: ms@2.1.3: {} - msw@2.3.5(typescript@5.6.2): + msw@2.3.5(typescript@5.6.3): dependencies: '@bundled-es-modules/cookie': 2.0.0 '@bundled-es-modules/statuses': 1.0.1 @@ -11197,7 +11197,7 @@ snapshots: type-fest: 4.11.1 yargs: 17.7.2 optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 mute-stream@1.0.0: {} @@ -11548,9 +11548,9 @@ snapshots: react-list: 0.8.17(react@18.3.1) shallow-equal: 1.2.1 - react-docgen-typescript@2.2.2(typescript@5.6.2): + react-docgen-typescript@2.2.2(typescript@5.6.3): dependencies: - typescript: 5.6.2 + typescript: 5.6.3 react-docgen@7.0.3: dependencies: @@ -12260,7 +12260,7 @@ snapshots: '@ts-morph/common': 0.12.3 code-block-writer: 11.0.3 - ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.2): + ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -12274,7 +12274,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.2 + typescript: 5.6.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: @@ -12348,7 +12348,7 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - typescript@5.6.2: {} + typescript@5.6.3: {} tzdata@1.0.40: {} @@ -12485,7 +12485,7 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-plugin-checker@0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.2)(vite@5.4.8(@types/node@20.16.10)): + vite-plugin-checker@0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)): dependencies: '@babel/code-frame': 7.25.7 ansi-escapes: 4.3.2 @@ -12506,7 +12506,7 @@ snapshots: '@biomejs/biome': 1.9.3 eslint: 8.52.0 optionator: 0.9.3 - typescript: 5.6.2 + typescript: 5.6.3 vite-plugin-turbosnap@1.0.3: {} From 6153880e9c7e76d0880ee40825af373a72c2d2c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:00:35 +0500 Subject: [PATCH 039/223] chore: bump @swc/jest from 0.2.36 to 0.2.37 in /site (#15331) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- site/package.json | 2 +- site/pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/site/package.json b/site/package.json index 8b102d662517a..beed6dc38de83 100644 --- a/site/package.json +++ b/site/package.json @@ -117,7 +117,7 @@ "@storybook/react-vite": "8.1.11", "@storybook/test": "8.1.11", "@swc/core": "1.3.38", - "@swc/jest": "0.2.36", + "@swc/jest": "0.2.37", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "14.3.1", "@testing-library/react-hooks": "8.0.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 9c2827f3cf94d..f9e0adaa2136d 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -260,8 +260,8 @@ importers: specifier: 1.3.38 version: 1.3.38 '@swc/jest': - specifier: 0.2.36 - version: 0.2.36(@swc/core@1.3.38) + specifier: 0.2.37 + version: 0.2.37(@swc/core@1.3.38) '@testing-library/jest-dom': specifier: 6.4.6 version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) @@ -357,7 +357,7 @@ importers: version: 2.5.0 jest_workaround: specifier: 0.1.14 - version: 0.1.14(@swc/core@1.3.38)(@swc/jest@0.2.36(@swc/core@1.3.38)) + version: 0.1.14(@swc/core@1.3.38)(@swc/jest@0.2.37(@swc/core@1.3.38)) msw: specifier: 2.3.5 version: 2.3.5(typescript@5.6.3) @@ -2246,8 +2246,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/jest@0.2.36': - resolution: {integrity: sha512-8X80dp81ugxs4a11z1ka43FPhP+/e+mJNXJSxiNYk8gIX/jPBtY4gQTrKu/KIoco8bzKuPI5lUxjfLiGsfvnlw==} + '@swc/jest@0.2.37': + resolution: {integrity: sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' @@ -8204,7 +8204,7 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/jest@0.2.36(@swc/core@1.3.38)': + '@swc/jest@0.2.37(@swc/core@1.3.38)': dependencies: '@jest/create-cache-key-function': 29.7.0 '@swc/core': 1.3.38 @@ -10620,10 +10620,10 @@ snapshots: - supports-color - ts-node - jest_workaround@0.1.14(@swc/core@1.3.38)(@swc/jest@0.2.36(@swc/core@1.3.38)): + jest_workaround@0.1.14(@swc/core@1.3.38)(@swc/jest@0.2.37(@swc/core@1.3.38)): dependencies: '@swc/core': 1.3.38 - '@swc/jest': 0.2.36(@swc/core@1.3.38) + '@swc/jest': 0.2.37(@swc/core@1.3.38) js-tokens@4.0.0: {} From b529393f6f0a44023ee2dcac13f8a8dfa42c1f64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:01:34 +0500 Subject: [PATCH 040/223] chore: bump @chakra-ui/react from 2.9.3 to 2.10.3 in /offlinedocs (#15326) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- offlinedocs/package.json | 2 +- offlinedocs/pnpm-lock.yaml | 126 +++++++++++-------------------------- 2 files changed, 38 insertions(+), 90 deletions(-) diff --git a/offlinedocs/package.json b/offlinedocs/package.json index c97a0006c4eb6..ea3dd060c01f1 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -13,7 +13,7 @@ "format:check": "prettier --cache --check './**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'" }, "dependencies": { - "@chakra-ui/react": "2.9.3", + "@chakra-ui/react": "2.10.3", "@emotion/react": "11.13.3", "@emotion/styled": "11.13.0", "archiver": "6.0.2", diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index 9a4d91652f804..0f7d7d44edd54 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@chakra-ui/react': - specifier: 2.9.3 - version: 2.9.3(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.10.3 + version: 2.10.3(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/react': specifier: 11.13.3 version: 11.13.3(@types/react@18.3.11)(react@18.3.1) @@ -127,16 +127,16 @@ packages: resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} - '@chakra-ui/anatomy@2.3.2': - resolution: {integrity: sha512-YRezCngYKigfIOLVszAL21lv1h+61pgxVGRu2rcsVGCbbvGSTkMhoML2/Yw2c03sEkrhBIVx1RtX+7550njaoA==} + '@chakra-ui/anatomy@2.3.4': + resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==} - '@chakra-ui/hooks@2.3.2': - resolution: {integrity: sha512-ETe3gJYf5my2ri8WKWLZYuwn+nYzItrTKss9pG5bSPmWEWG4qWP+Zjl6hnfMI11c1dfUatpQZZqd4KsFQVNWRA==} + '@chakra-ui/hooks@2.4.2': + resolution: {integrity: sha512-LRKiVE1oA7afT5tbbSKAy7Uas2xFHE6IkrQdbhWCHmkHBUtPvjQQDgwtnd4IRZPmoEfNGwoJ/MQpwOM/NRTTwA==} peerDependencies: react: '>=18' - '@chakra-ui/react@2.9.3': - resolution: {integrity: sha512-ccg0CVgAqKtU/xb1w86+A2XC/56g8AUCNKYG0SrI0P89WGYHsM+5xHiuUgtPeIg0vuuQS8WZupm9BgiPIp67Og==} + '@chakra-ui/react@2.10.3': + resolution: {integrity: sha512-oWmGGzzKWBfoB3hrrQxWwFirlxJXGdk3v4SLnLPPYRy9IMibmQM5rAUJ/NxZum1mrYGP5lo7DHcIWDfV2A3ubw==} peerDependencies: '@emotion/react': '>=11' '@emotion/styled': '>=11' @@ -144,21 +144,21 @@ packages: react: '>=18' react-dom: '>=18' - '@chakra-ui/styled-system@2.10.2': - resolution: {integrity: sha512-MXV/oahBBoWroZmLqIERQE3a2cJrb0iu+Evv1km3pCw/gtA/mhV3ogEIZt0oUEblcE0Nh80FwTTRZOJhboUZXw==} + '@chakra-ui/styled-system@2.12.0': + resolution: {integrity: sha512-zoqLw1I2y4GlZ0LDoyw8o0JjoDOW6u0IwFPAoHuw0UMbP8glHUGvwEL1STug/i/GzBKw83yoF6ae41HIQvhMww==} - '@chakra-ui/theme-tools@2.2.2': - resolution: {integrity: sha512-iK9xoIEnEO3mXSDUsnCNigFifgRU3K8fhuIrN+q20V7YxenrifqJ7wAbbALHxy7Awrfe3NOqdVca2lnsQ2L4Ig==} + '@chakra-ui/theme-tools@2.2.6': + resolution: {integrity: sha512-3UhKPyzKbV3l/bg1iQN9PBvffYp+EBOoYMUaeTUdieQRPFzo2jbYR0lNCxqv8h5aGM/k54nCHU2M/GStyi9F2A==} peerDependencies: '@chakra-ui/styled-system': '>=2.0.0' - '@chakra-ui/theme@3.4.2': - resolution: {integrity: sha512-iZ9WelkjJ7VJzWCDFpiYaAxGodW8Bahz+YrGp3P/CKsQrH1yOVHE9R190H9eiiSxw7tOyniKbMdd31GI8HaYtA==} + '@chakra-ui/theme@3.4.6': + resolution: {integrity: sha512-ZwFBLfiMC3URwaO31ONXoKH9k0TX0OW3UjdPF3EQkQpYyrk/fm36GkkzajjtdpWEd7rzDLRsQjPmvwNaSoNDtg==} peerDependencies: '@chakra-ui/styled-system': '>=2.8.0' - '@chakra-ui/utils@2.1.2': - resolution: {integrity: sha512-zByY3e1SUjNJ8jLjpOEaM2r+j6fPyh2XxPFHKDwbuzgSRrsJX+5BGozXfh8ZhmIJmSuByOWnWvGDSWxl3gCqag==} + '@chakra-ui/utils@2.2.2': + resolution: {integrity: sha512-jUPLT0JzRMWxpdzH6c+t0YMJYrvc5CLericgITV3zDSXblkfx3DsYXqU11DJTSGZI9dUKzM1Wd0Wswn4eJwvFQ==} peerDependencies: react: '>=16.8.0' @@ -738,9 +738,6 @@ packages: resolution: {integrity: sha512-lO1dFui+CEUh/ztYIpgpKItKW9Bb4NWakCRJrnqAbFIYD+OZAwb2VfD5T5eXMw2FNcsDHkQcNl/Wh3iVXYwU6g==} engines: {node: '>= 12.0.0'} - create-react-class@15.7.0: - resolution: {integrity: sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1472,10 +1469,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lorem-ipsum@1.0.6: - resolution: {integrity: sha512-Rx4XH8X4KSDCKAVvWGYlhAfNqdUP5ZdT4rRyf0jjrvWgtViZimDIlopWNfn/y3lGM5K4uuiAoY28TaD+7YKFrQ==} - hasBin: true - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1862,11 +1855,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-lorem-component@0.13.0: - resolution: {integrity: sha512-4mWjxmcG/DJJwdxdKwXWyP2N9zohbJg/yYaC+7JffQNrKj3LYDpA/A4u/Dju1v1ZF6Jew2gbFKGb5Z6CL+UNTw==} - peerDependencies: - react: 16.x - react-markdown@9.0.1: resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: @@ -1990,9 +1978,6 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - seedable-random@0.0.1: - resolution: {integrity: sha512-uZWbEfz3BQdBl4QlUPELPqhInGEO1Q6zjzqrTDkd3j7mHaWWJo7h4ydr2g24a2WtTLk3imTLc8mPbBdQqdsbGw==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2262,16 +2247,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-callback-ref@1.3.0: - resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - use-callback-ref@1.3.2: resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} @@ -2418,22 +2393,22 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - '@chakra-ui/anatomy@2.3.2': {} + '@chakra-ui/anatomy@2.3.4': {} - '@chakra-ui/hooks@2.3.2(react@18.3.1)': + '@chakra-ui/hooks@2.4.2(react@18.3.1)': dependencies: - '@chakra-ui/utils': 2.1.2(react@18.3.1) + '@chakra-ui/utils': 2.2.2(react@18.3.1) '@zag-js/element-size': 0.31.1 copy-to-clipboard: 3.3.3 framesync: 6.1.2 react: 18.3.1 - '@chakra-ui/react@2.9.3(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@chakra-ui/react@2.10.3(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@chakra-ui/hooks': 2.3.2(react@18.3.1) - '@chakra-ui/styled-system': 2.10.2(react@18.3.1) - '@chakra-ui/theme': 3.4.2(@chakra-ui/styled-system@2.10.2(react@18.3.1))(react@18.3.1) - '@chakra-ui/utils': 2.1.2(react@18.3.1) + '@chakra-ui/hooks': 2.4.2(react@18.3.1) + '@chakra-ui/styled-system': 2.12.0(react@18.3.1) + '@chakra-ui/theme': 3.4.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1) + '@chakra-ui/utils': 2.2.2(react@18.3.1) '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@popperjs/core': 2.11.8 @@ -2444,37 +2419,36 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-fast-compare: 3.2.2 react-focus-lock: 2.13.2(@types/react@18.3.11)(react@18.3.1) - react-lorem-component: 0.13.0(react@18.3.1) react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1) transitivePeerDependencies: - '@types/react' - '@chakra-ui/styled-system@2.10.2(react@18.3.1)': + '@chakra-ui/styled-system@2.12.0(react@18.3.1)': dependencies: - '@chakra-ui/utils': 2.1.2(react@18.3.1) + '@chakra-ui/utils': 2.2.2(react@18.3.1) csstype: 3.1.3 transitivePeerDependencies: - react - '@chakra-ui/theme-tools@2.2.2(@chakra-ui/styled-system@2.10.2(react@18.3.1))(react@18.3.1)': + '@chakra-ui/theme-tools@2.2.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1)': dependencies: - '@chakra-ui/anatomy': 2.3.2 - '@chakra-ui/styled-system': 2.10.2(react@18.3.1) - '@chakra-ui/utils': 2.1.2(react@18.3.1) + '@chakra-ui/anatomy': 2.3.4 + '@chakra-ui/styled-system': 2.12.0(react@18.3.1) + '@chakra-ui/utils': 2.2.2(react@18.3.1) color2k: 2.0.2 transitivePeerDependencies: - react - '@chakra-ui/theme@3.4.2(@chakra-ui/styled-system@2.10.2(react@18.3.1))(react@18.3.1)': + '@chakra-ui/theme@3.4.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1)': dependencies: - '@chakra-ui/anatomy': 2.3.2 - '@chakra-ui/styled-system': 2.10.2(react@18.3.1) - '@chakra-ui/theme-tools': 2.2.2(@chakra-ui/styled-system@2.10.2(react@18.3.1))(react@18.3.1) - '@chakra-ui/utils': 2.1.2(react@18.3.1) + '@chakra-ui/anatomy': 2.3.4 + '@chakra-ui/styled-system': 2.12.0(react@18.3.1) + '@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1) + '@chakra-ui/utils': 2.2.2(react@18.3.1) transitivePeerDependencies: - react - '@chakra-ui/utils@2.1.2(react@18.3.1)': + '@chakra-ui/utils@2.2.2(react@18.3.1)': dependencies: '@types/lodash.mergewith': 4.6.9 lodash.mergewith: 4.6.2 @@ -3119,11 +3093,6 @@ snapshots: crc-32: 1.2.2 readable-stream: 3.6.2 - create-react-class@15.7.0: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4040,10 +4009,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - lorem-ipsum@1.0.6: - dependencies: - minimist: 1.2.8 - lru-cache@10.4.3: {} markdown-table@3.0.3: {} @@ -4646,14 +4611,6 @@ snapshots: react-is@16.13.1: {} - react-lorem-component@0.13.0(react@18.3.1): - dependencies: - create-react-class: 15.7.0 - lorem-ipsum: 1.0.6 - object-assign: 4.1.1 - react: 18.3.1 - seedable-random: 0.0.1 - react-markdown@9.0.1(@types/react@18.3.11)(react@18.3.1): dependencies: '@types/hast': 3.0.3 @@ -4685,7 +4642,7 @@ snapshots: react-remove-scroll-bar: 2.3.6(@types/react@18.3.11)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) tslib: 2.6.2 - use-callback-ref: 1.3.0(@types/react@18.3.11)(react@18.3.1) + use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1) optionalDependencies: '@types/react': 18.3.11 @@ -4831,8 +4788,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - seedable-random@0.0.1: {} - semver@6.3.1: {} semver@7.6.3: {} @@ -5115,13 +5070,6 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.0(@types/react@18.3.11)(react@18.3.1): - dependencies: - react: 18.3.1 - tslib: 2.6.2 - optionalDependencies: - '@types/react': 18.3.11 - use-callback-ref@1.3.2(@types/react@18.3.11)(react@18.3.1): dependencies: react: 18.3.1 From 065263a852d0d66995f0e4177ca0477bec8fc04e Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 3 Nov 2024 22:01:54 -0800 Subject: [PATCH 041/223] chore: update dependabot config and pin Docker images (#15194) --- .github/dependabot.yaml | 11 ++++++++++- .github/workflows/pr-deploy.yaml | 10 ++++++++++ dogfood/contents/Dockerfile | 6 +++--- examples/jfrog/docker/build/Dockerfile | 2 +- scaletest/templates/scaletest-runner/Dockerfile | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 3bdc208efd3ca..68539f0f4088f 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -51,7 +51,13 @@ updates: # Update our Dockerfile. - package-ecosystem: "docker" - directory: "/scripts/" + directories: + - "/dogfood/contents" + - "/scripts" + - "/examples/templates/docker/build" + - "/examples/parameters/build" + - "/scaletest/templates/scaletest-runner" + - "/scripts/ironbank" schedule: interval: "weekly" time: "06:00" @@ -68,6 +74,9 @@ updates: directories: - "/site" - "/offlinedocs" + - "/scripts" + - "/scripts/apidocgen" + schedule: interval: "monthly" time: "06:00" diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 49e73e9b0bf63..fc59ae85289e3 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -216,6 +216,11 @@ jobs: DOCKER_CLI_EXPERIMENTAL: "enabled" CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + - name: Checkout uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: @@ -267,6 +272,11 @@ jobs: PR_URL: ${{ needs.get_info.outputs.PR_URL }} PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + - name: Set up kubeconfig run: | set -euo pipefail diff --git a/dogfood/contents/Dockerfile b/dogfood/contents/Dockerfile index bef5bccbaa423..4d43e2f21e8b4 100644 --- a/dogfood/contents/Dockerfile +++ b/dogfood/contents/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:slim AS rust-utils +FROM rust:slim@sha256:9abf10cc84dfad6ace1b0aae3951dc5200f467c593394288c11db1e17bb4d349 AS rust-utils # Install rust helper programs # ENV CARGO_NET_GIT_FETCH_WITH_CLI=true ENV CARGO_INSTALL_ROOT=/tmp/ @@ -6,7 +6,7 @@ RUN cargo install exa bat ripgrep typos-cli watchexec-cli && \ # Reduce image size. rm -rf /usr/local/cargo/registry -FROM ubuntu:jammy AS go +FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go # Install Go manually, so that we can control the version ARG GO_VERSION=1.22.5 @@ -94,7 +94,7 @@ RUN curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/d unzip protoc.zip && \ rm protoc.zip -FROM ubuntu:jammy +FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 SHELL ["/bin/bash", "-c"] diff --git a/examples/jfrog/docker/build/Dockerfile b/examples/jfrog/docker/build/Dockerfile index ff627a010a464..69fbb54eaf794 100644 --- a/examples/jfrog/docker/build/Dockerfile +++ b/examples/jfrog/docker/build/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu +FROM ubuntu@sha256:99c35190e22d294cdace2783ac55effc69d32896daaa265f0bbedbcde4fbe3e5 RUN apt-get update \ && apt-get install -y \ diff --git a/scaletest/templates/scaletest-runner/Dockerfile b/scaletest/templates/scaletest-runner/Dockerfile index 9aa016b534a17..61409c1018654 100644 --- a/scaletest/templates/scaletest-runner/Dockerfile +++ b/scaletest/templates/scaletest-runner/Dockerfile @@ -5,7 +5,7 @@ # Future improvements will include versioning and including the version # in the template push. -FROM codercom/enterprise-base:ubuntu +FROM codercom/enterprise-base:ubuntu@sha256:22837dba6f92f075c29797652699df748ec223e04dc87627f3d2bae0a6bce7bd ARG DEBIAN_FRONTEND=noninteractive From 3a5a42ffa985b92fed323186a5ecab6ef1f3ba3d Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 3 Nov 2024 22:09:40 -0800 Subject: [PATCH 042/223] chore: update workflow permissions (#15349) --- .github/workflows/ci.yaml | 11 ++--------- .github/workflows/contrib.yaml | 3 +++ .github/workflows/pr-cleanup.yaml | 3 +++ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e6d105d8890f4..7773ac759e0d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,16 +9,7 @@ on: workflow_dispatch: permissions: - actions: none - checks: none contents: read - deployments: none - issues: none - packages: write - pull-requests: none - repository-projects: none - security-events: none - statuses: none # Cancel in-progress runs for pull requests when developers push # additional changes @@ -821,6 +812,8 @@ jobs: needs: changes if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} + permissions: + packages: write # Needed to push images to ghcr.io env: DOCKER_CLI_EXPERIMENTAL: "enabled" outputs: diff --git a/.github/workflows/contrib.yaml b/.github/workflows/contrib.yaml index 3389042cea18c..cd86a2d0309d9 100644 --- a/.github/workflows/contrib.yaml +++ b/.github/workflows/contrib.yaml @@ -16,6 +16,9 @@ on: # For jobs that don't run on draft PRs. - ready_for_review +permissions: + contents: read + # Only run one instance per PR to ensure in-order execution. concurrency: pr-${{ github.ref }} diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index f5cee03a4c6c4..2a97eb29a67b7 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -8,6 +8,9 @@ on: description: "PR number" required: true +permissions: + contents: read + jobs: cleanup: runs-on: "ubuntu-latest" From 98bb560f63149eaa801ecd6250eb8de399688994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 06:11:44 +0000 Subject: [PATCH 043/223] chore: bump the react group across 2 directories with 2 updates (#15319) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- offlinedocs/package.json | 4 +- offlinedocs/pnpm-lock.yaml | 92 ++++----- site/package.json | 4 +- site/pnpm-lock.yaml | 392 ++++++++++++++++++------------------- 4 files changed, 246 insertions(+), 246 deletions(-) diff --git a/offlinedocs/package.json b/offlinedocs/package.json index ea3dd060c01f1..ca0e259eb6b36 100644 --- a/offlinedocs/package.json +++ b/offlinedocs/package.json @@ -31,8 +31,8 @@ "devDependencies": { "@types/lodash": "4.17.13", "@types/node": "20.16.10", - "@types/react": "18.3.11", - "@types/react-dom": "18.3.0", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", "eslint": "8.57.1", "eslint-config-next": "14.2.16", "prettier": "3.3.3", diff --git a/offlinedocs/pnpm-lock.yaml b/offlinedocs/pnpm-lock.yaml index 0f7d7d44edd54..d9fc29d255baf 100644 --- a/offlinedocs/pnpm-lock.yaml +++ b/offlinedocs/pnpm-lock.yaml @@ -10,13 +10,13 @@ importers: dependencies: '@chakra-ui/react': specifier: 2.10.3 - version: 2.10.3(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.10.3(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/react': specifier: 11.13.3 - version: 11.13.3(@types/react@18.3.11)(react@18.3.1) + version: 11.13.3(@types/react@18.3.12)(react@18.3.1) '@emotion/styled': specifier: 11.13.0 - version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) archiver: specifier: 6.0.2 version: 6.0.2 @@ -43,7 +43,7 @@ importers: version: 4.12.0(react@18.3.1) react-markdown: specifier: 9.0.1 - version: 9.0.1(@types/react@18.3.11)(react@18.3.1) + version: 9.0.1(@types/react@18.3.12)(react@18.3.1) rehype-raw: specifier: 7.0.0 version: 7.0.0 @@ -58,11 +58,11 @@ importers: specifier: 20.16.10 version: 20.16.10 '@types/react': - specifier: 18.3.11 - version: 18.3.11 + specifier: 18.3.12 + version: 18.3.12 '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 + specifier: 18.3.1 + version: 18.3.1 eslint: specifier: 8.57.1 version: 8.57.1 @@ -403,11 +403,11 @@ packages: '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} - '@types/react-dom@18.3.0': - resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} - '@types/react@18.3.11': - resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -2403,14 +2403,14 @@ snapshots: framesync: 6.1.2 react: 18.3.1 - '@chakra-ui/react@2.10.3(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@chakra-ui/react@2.10.3(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@chakra-ui/hooks': 2.4.2(react@18.3.1) '@chakra-ui/styled-system': 2.12.0(react@18.3.1) '@chakra-ui/theme': 3.4.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1) '@chakra-ui/utils': 2.2.2(react@18.3.1) - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@popperjs/core': 2.11.8 '@zag-js/focus-visible': 0.31.1 aria-hidden: 1.2.3 @@ -2418,8 +2418,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-fast-compare: 3.2.2 - react-focus-lock: 2.13.2(@types/react@18.3.11)(react@18.3.1) - react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1) + react-focus-lock: 2.13.2(@types/react@18.3.12)(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) transitivePeerDependencies: - '@types/react' @@ -2494,7 +2494,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1)': + '@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 '@emotion/babel-plugin': 11.12.0 @@ -2506,7 +2506,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 transitivePeerDependencies: - supports-color @@ -2520,18 +2520,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': + '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 '@emotion/babel-plugin': 11.12.0 '@emotion/is-prop-valid': 1.3.0 - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) '@emotion/serialize': 1.3.1 '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) '@emotion/utils': 1.4.0 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 transitivePeerDependencies: - supports-color @@ -2710,11 +2710,11 @@ snapshots: '@types/prop-types@15.7.13': {} - '@types/react-dom@18.3.0': + '@types/react-dom@18.3.1': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@types/react@18.3.11': + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.13 csstype: 3.1.3 @@ -4593,17 +4593,17 @@ snapshots: react-fast-compare@3.2.2: {} - react-focus-lock@2.13.2(@types/react@18.3.11)(react@18.3.1): + react-focus-lock@2.13.2(@types/react@18.3.12)(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 focus-lock: 1.3.5 prop-types: 15.8.1 react: 18.3.1 react-clientside-effect: 1.2.6(react@18.3.1) - use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1) + use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 react-icons@4.12.0(react@18.3.1): dependencies: @@ -4611,10 +4611,10 @@ snapshots: react-is@16.13.1: {} - react-markdown@9.0.1(@types/react@18.3.11)(react@18.3.1): + react-markdown@9.0.1(@types/react@18.3.12)(react@18.3.1): dependencies: '@types/hast': 3.0.3 - '@types/react': 18.3.11 + '@types/react': 18.3.12 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.0 html-url-attributes: 3.0.0 @@ -4628,33 +4628,33 @@ snapshots: transitivePeerDependencies: - supports-color - react-remove-scroll-bar@2.3.6(@types/react@18.3.11)(react@18.3.1): + react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - react-remove-scroll@2.6.0(@types/react@18.3.11)(react@18.3.1): + react-remove-scroll@2.6.0(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.6(@types/react@18.3.11)(react@18.3.1) - react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) + react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) tslib: 2.6.2 - use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1) + use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - react-style-singleton@2.2.1(@types/react@18.3.11)(react@18.3.1): + react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.3.1): dependencies: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 react@18.3.1: dependencies: @@ -5070,20 +5070,20 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.2(@types/react@18.3.11)(react@18.3.1): + use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - use-sidecar@1.1.2(@types/react@18.3.11)(react@18.3.1): + use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 util-deprecate@1.0.2: {} diff --git a/site/package.json b/site/package.json index beed6dc38de83..799c985ffad2c 100644 --- a/site/package.json +++ b/site/package.json @@ -129,10 +129,10 @@ "@types/jest": "29.5.14", "@types/lodash": "4.17.9", "@types/node": "20.16.10", - "@types/react": "18.3.11", + "@types/react": "18.3.12", "@types/react-color": "3.0.12", "@types/react-date-range": "1.4.4", - "@types/react-dom": "18.3.0", + "@types/react-dom": "18.3.1", "@types/react-syntax-highlighter": "15.5.13", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/react-window": "1.8.8", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index f9e0adaa2136d..5823a5f5ee144 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -26,10 +26,10 @@ importers: version: 11.13.4 '@emotion/react': specifier: 11.13.3 - version: 11.13.3(@types/react@18.3.11)(react@18.3.1) + version: 11.13.3(@types/react@18.3.12)(react@18.3.1) '@emotion/styled': specifier: 11.13.0 - version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@fastly/performance-observer-polyfill': specifier: 2.0.0 version: 2.0.0 @@ -44,22 +44,22 @@ importers: version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/icons-material': specifier: 5.16.7 - version: 5.16.7(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + version: 5.16.7(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/lab': specifier: 5.0.0-alpha.173 - version: 5.0.0-alpha.173(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.0.0-alpha.173(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': specifier: 5.16.7 - version: 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/system': specifier: 5.16.7 - version: 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + version: 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/utils': specifier: 5.16.6 - version: 5.16.6(@types/react@18.3.11)(react@18.3.1) + version: 5.16.6(@types/react@18.3.12)(react@18.3.1) '@mui/x-tree-view': specifier: 7.18.0 - version: 7.18.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 7.18.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query-devtools': specifier: 4.35.3 version: 4.35.3(@tanstack/react-query@4.35.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -164,7 +164,7 @@ importers: version: 2.0.5(react@18.3.1) react-markdown: specifier: 9.0.1 - version: 9.0.1(@types/react@18.3.11)(react@18.3.1) + version: 9.0.1(@types/react@18.3.12)(react@18.3.1) react-query: specifier: npm:@tanstack/react-query@4.35.3 version: '@tanstack/react-query@4.35.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' @@ -231,7 +231,7 @@ importers: version: 8.3.5(storybook@8.3.5) '@storybook/addon-essentials': specifier: 8.1.11 - version: 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-interactions': specifier: 8.1.11 version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) @@ -270,7 +270,7 @@ importers: version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/react-hooks': specifier: 8.0.1 - version: 8.0.1(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': specifier: 14.5.1 version: 14.5.1(@testing-library/dom@10.1.0) @@ -296,8 +296,8 @@ importers: specifier: 20.16.10 version: 20.16.10 '@types/react': - specifier: 18.3.11 - version: 18.3.11 + specifier: 18.3.12 + version: 18.3.12 '@types/react-color': specifier: 3.0.12 version: 3.0.12 @@ -305,8 +305,8 @@ importers: specifier: 1.4.4 version: 1.4.4 '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 + specifier: 18.3.1 + version: 18.3.1 '@types/react-syntax-highlighter': specifier: 15.5.13 version: 15.5.13 @@ -378,7 +378,7 @@ importers: version: 8.3.5 storybook-addon-remix-react-router: specifier: 3.0.1 - version: 3.0.1(@storybook/blocks@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/channels@8.1.11)(@storybook/components@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.1.11)(@storybook/theming@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 3.0.1(@storybook/blocks@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/channels@8.1.11)(@storybook/components@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.1.11)(@storybook/theming@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) storybook-react-context: specifier: 0.6.0 version: 0.6.0(react-dom@18.3.1(react@18.3.1)) @@ -2556,8 +2556,8 @@ packages: '@types/react-date-range@1.4.4': resolution: {integrity: sha512-9Y9NyNgaCsEVN/+O4HKuxzPbVjRVBGdOKRxMDcsTRWVG62lpYgnxefNckTXDWup8FvczoqPW0+ESZR6R1yymDg==} - '@types/react-dom@18.3.0': - resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} @@ -2571,8 +2571,8 @@ packages: '@types/react-window@1.8.8': resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} - '@types/react@18.3.11': - resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} '@types/reactcss@1.2.6': resolution: {integrity: sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==} @@ -6509,7 +6509,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1)': + '@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 '@emotion/babel-plugin': 11.12.0 @@ -6521,7 +6521,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 transitivePeerDependencies: - supports-color @@ -6543,18 +6543,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': + '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 '@emotion/babel-plugin': 11.12.0 '@emotion/is-prop-valid': 1.3.0 - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) '@emotion/serialize': 1.3.1 '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) '@emotion/utils': 1.4.0 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 transitivePeerDependencies: - supports-color @@ -7125,10 +7125,10 @@ snapshots: '@leeoniya/ufuzzy@1.0.10': {} - '@mdx-js/react@3.0.1(@types/react@18.3.11)(react@18.3.1)': + '@mdx-js/react@3.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: '@types/mdx': 2.0.9 - '@types/react': 18.3.11 + '@types/react': 18.3.12 react: 18.3.1 '@monaco-editor/loader@1.4.0(monaco-editor@0.52.0)': @@ -7152,54 +7152,54 @@ snapshots: outvariant: 1.4.2 strict-event-emitter: 0.5.1 - '@mui/base@5.0.0-beta.40(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/base@5.0.0-beta.40(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.11) - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.12) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) '@popperjs/core': 2.11.8 clsx: 2.1.1 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@mui/core-downloads-tracker@5.16.7': {} - '@mui/icons-material@5.16.7(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': + '@mui/icons-material@5.16.7(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 - '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@mui/lab@5.0.0-alpha.173(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/lab@5.0.0-alpha.173(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 - '@mui/base': 5.0.0-beta.40(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.11) - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) + '@mui/base': 5.0.0-beta.40(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.12) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) clsx: 2.1.1 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@types/react': 18.3.11 + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 - '@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 '@mui/core-downloads-tracker': 5.16.7 - '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.11) - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) + '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.12) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.11 clsx: 2.1.1 @@ -7210,20 +7210,20 @@ snapshots: react-is: 18.3.1 react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@types/react': 18.3.11 + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 - '@mui/private-theming@5.16.6(@types/react@18.3.11)(react@18.3.1)': + '@mui/private-theming@5.16.6(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) prop-types: 15.8.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@mui/styled-engine@5.16.6(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(react@18.3.1)': + '@mui/styled-engine@5.16.6(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 '@emotion/cache': 11.13.1 @@ -7231,56 +7231,56 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) - '@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': + '@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 - '@mui/private-theming': 5.16.6(@types/react@18.3.11)(react@18.3.1) - '@mui/styled-engine': 5.16.6(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.11) - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) + '@mui/private-theming': 5.16.6(@types/react@18.3.12)(react@18.3.1) + '@mui/styled-engine': 5.16.6(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.12) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 18.3.1 optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@types/react': 18.3.11 + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 - '@mui/types@7.2.15(@types/react@18.3.11)': + '@mui/types@7.2.15(@types/react@18.3.12)': optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@mui/utils@5.16.6(@types/react@18.3.11)(react@18.3.1)': + '@mui/utils@5.16.6(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 - '@mui/types': 7.2.15(@types/react@18.3.11) + '@mui/types': 7.2.15(@types/react@18.3.12) '@types/prop-types': 15.7.12 clsx: 2.1.1 prop-types: 15.8.1 react: 18.3.1 react-is: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@mui/x-internals@7.18.0(@types/react@18.3.11)(react@18.3.1)': + '@mui/x-internals@7.18.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 transitivePeerDependencies: - '@types/react' - '@mui/x-tree-view@7.18.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/x-tree-view@7.18.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 - '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1) - '@mui/x-internals': 7.18.0(@types/react@18.3.11)(react@18.3.1) + '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + '@mui/utils': 5.16.6(@types/react@18.3.12)(react@18.3.1) + '@mui/x-internals': 7.18.0(@types/react@18.3.12)(react@18.3.1) '@types/react-transition-group': 4.4.11 clsx: 2.1.1 prop-types: 15.8.1 @@ -7288,8 +7288,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.12)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) transitivePeerDependencies: - '@types/react' @@ -7354,153 +7354,153 @@ snapshots: '@radix-ui/primitive@1.1.0': {} - '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-context@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-context@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-context': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.7(@types/react@18.3.11)(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.12)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 - '@types/react-dom': 18.3.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 - '@types/react-dom': 18.3.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 - '@types/react-dom': 18.3.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-id@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-id@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 - '@types/react-dom': 18.3.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 - '@types/react-dom': 18.3.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 - '@types/react-dom': 18.3.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 - '@radix-ui/react-slot@1.0.2(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-slot@1.0.2(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-slot@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.11)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@remix-run/router@1.19.2': {} @@ -7594,9 +7594,9 @@ snapshots: memoizerific: 1.11.3 ts-dedent: 2.2.0 - '@storybook/addon-controls@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/addon-controls@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@storybook/blocks': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/blocks': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) dequal: 2.0.3 lodash: 4.17.21 ts-dedent: 2.2.0 @@ -7609,13 +7609,13 @@ snapshots: - react-dom - supports-color - '@storybook/addon-docs@8.1.11(@types/react-dom@18.3.0)(prettier@3.3.3)': + '@storybook/addon-docs@8.1.11(@types/react-dom@18.3.1)(prettier@3.3.3)': dependencies: '@babel/core': 7.25.8 - '@mdx-js/react': 3.0.1(@types/react@18.3.11)(react@18.3.1) - '@storybook/blocks': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mdx-js/react': 3.0.1(@types/react@18.3.12)(react@18.3.1) + '@storybook/blocks': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/client-logger': 8.1.11 - '@storybook/components': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/csf-plugin': 8.1.11 '@storybook/csf-tools': 8.1.11 '@storybook/global': 5.0.0 @@ -7624,7 +7624,7 @@ snapshots: '@storybook/react-dom-shim': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/theming': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/types': 8.1.11 - '@types/react': 18.3.11 + '@types/react': 18.3.12 fs-extra: 11.2.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -7637,12 +7637,12 @@ snapshots: - prettier - supports-color - '@storybook/addon-essentials@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/addon-essentials@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@storybook/addon-actions': 8.1.11 '@storybook/addon-backgrounds': 8.1.11 - '@storybook/addon-controls': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/addon-docs': 8.1.11(@types/react-dom@18.3.0)(prettier@3.3.3) + '@storybook/addon-controls': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/addon-docs': 8.1.11(@types/react-dom@18.3.1)(prettier@3.3.3) '@storybook/addon-highlight': 8.1.11 '@storybook/addon-measure': 8.1.11 '@storybook/addon-outline': 8.1.11 @@ -7755,11 +7755,11 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/blocks@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/blocks@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@storybook/channels': 8.1.11 '@storybook/client-logger': 8.1.11 - '@storybook/components': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/core-events': 8.1.11 '@storybook/csf': 0.1.9 '@storybook/docs-tools': 8.1.11(prettier@3.3.3) @@ -7841,10 +7841,10 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/components@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/components@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1) '@storybook/client-logger': 8.1.11 '@storybook/csf': 0.1.9 '@storybook/global': 5.0.0 @@ -8286,20 +8286,20 @@ snapshots: '@types/jest': 29.5.14 jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) - '@testing-library/react-hooks@8.0.1(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react-hooks@8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.22.6 react: 18.3.1 react-error-boundary: 3.1.4(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 react-dom: 18.3.1(react@18.3.1) '@testing-library/react@14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 '@testing-library/dom': 9.3.3 - '@types/react-dom': 18.3.0 + '@types/react-dom': 18.3.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8436,7 +8436,7 @@ snapshots: '@types/hoist-non-react-statics@3.3.5': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 hoist-non-react-statics: 3.3.2 '@types/http-errors@2.0.1': {} @@ -8516,42 +8516,42 @@ snapshots: '@types/react-color@3.0.12': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@types/reactcss': 1.2.6 '@types/react-date-range@1.4.4': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 date-fns: 2.30.0 - '@types/react-dom@18.3.0': + '@types/react-dom@18.3.1': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@types/react-transition-group@4.4.11': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@types/react-virtualized-auto-sizer@1.0.4': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@types/react-window@1.8.8': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - '@types/react@18.3.11': + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.13 csstype: 3.1.3 '@types/reactcss@1.2.6': dependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 '@types/resolve@1.20.4': {} @@ -11614,10 +11614,10 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - react-markdown@9.0.1(@types/react@18.3.11)(react@18.3.1): + react-markdown@9.0.1(@types/react@18.3.12)(react@18.3.1): dependencies: '@types/hast': 3.0.3 - '@types/react': 18.3.11 + '@types/react': 18.3.12 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.2.0 html-url-attributes: 3.0.0 @@ -11633,24 +11633,24 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.6(@types/react@18.3.11)(react@18.3.1): + react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - react-remove-scroll@2.5.7(@types/react@18.3.11)(react@18.3.1): + react-remove-scroll@2.5.7(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.6(@types/react@18.3.11)(react@18.3.1) - react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) + react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) tslib: 2.6.2 - use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1) + use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -11664,14 +11664,14 @@ snapshots: '@remix-run/router': 1.19.2 react: 18.3.1 - react-style-singleton@2.2.1(@types/react@18.3.11)(react@18.3.1): + react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.3.1): dependencies: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 react-syntax-highlighter@15.5.0(react@18.3.1): dependencies: @@ -12044,11 +12044,11 @@ snapshots: store2@2.14.2: {} - storybook-addon-remix-react-router@3.0.1(@storybook/blocks@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/channels@8.1.11)(@storybook/components@8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.1.11)(@storybook/theming@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + storybook-addon-remix-react-router@3.0.1(@storybook/blocks@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/channels@8.1.11)(@storybook/components@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.1.11)(@storybook/manager-api@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.1.11)(@storybook/theming@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - '@storybook/blocks': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/blocks': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/channels': 8.1.11 - '@storybook/components': 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/core-events': 8.1.11 '@storybook/manager-api': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/preview-api': 8.1.11 @@ -12431,20 +12431,20 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-callback-ref@1.3.2(@types/react@18.3.11)(react@18.3.1): + use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 - use-sidecar@1.1.2(@types/react@18.3.11)(react@18.3.1): + use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.6.2 optionalDependencies: - '@types/react': 18.3.11 + '@types/react': 18.3.12 use-sync-external-store@1.2.0(react@18.3.1): dependencies: From 56326307f25bd2363983ecbc655fc1eadd47a457 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:33:04 +0500 Subject: [PATCH 044/223] chore: bump github.com/hashicorp/terraform-json from 0.22.1 to 0.23.0 (#15355) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cf3b533b35674..c9a7ec6a57877 100644 --- a/go.mod +++ b/go.mod @@ -125,7 +125,7 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hc-install v0.9.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f - github.com/hashicorp/terraform-json v0.22.1 + github.com/hashicorp/terraform-json v0.23.0 github.com/hashicorp/yamux v0.1.1 github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 github.com/imulab/go-scim/pkg/v2 v2.2.0 diff --git a/go.sum b/go.sum index 771268286eebe..5f0dabf683784 100644 --- a/go.sum +++ b/go.sum @@ -581,8 +581,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= -github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= -github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= +github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= From 4a8fe424df96f76905fd700f3a884bb599a238e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:33:18 -0800 Subject: [PATCH 045/223] chore: bump github.com/open-policy-agent/opa from 0.69.0 to 0.70.0 (#15358) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c9a7ec6a57877..897e2d5e50138 100644 --- a/go.mod +++ b/go.mod @@ -141,13 +141,13 @@ require ( github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c github.com/moby/moby v27.3.1+incompatible github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a - github.com/open-policy-agent/opa v0.69.0 + github.com/open-policy-agent/opa v0.70.0 github.com/ory/dockertest/v3 v3.11.0 github.com/pion/udp v0.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/sftp v1.13.6 - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.60.0 github.com/quasilyte/go-ruleguard/dsl v0.3.21 diff --git a/go.sum b/go.sum index 5f0dabf683784..3caa0d8efcb83 100644 --- a/go.sum +++ b/go.sum @@ -779,8 +779,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/open-policy-agent/opa v0.69.0 h1:s2igLw2Z6IvGWGuXSfugWkVultDMsM9pXiDuMp7ckWw= -github.com/open-policy-agent/opa v0.69.0/go.mod h1:+qyXJGkpEJ6kpB1kGo8JSwHtVXbTdsGdQYPWWNYNj+4= +github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U= +github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -824,8 +824,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= From a1b03fa6d23b9046bf0da2444d3408e2527a046b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:33:33 +0500 Subject: [PATCH 046/223] chore: bump google.golang.org/api from 0.203.0 to 0.204.0 (#15356) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 897e2d5e50138..4ccd0fcbbc455 100644 --- a/go.mod +++ b/go.mod @@ -185,7 +185,7 @@ require ( golang.org/x/text v0.19.0 golang.org/x/tools v0.26.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da - google.golang.org/api v0.203.0 + google.golang.org/api v0.204.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 gopkg.in/DataDog/dd-trace-go.v1 v1.69.0 @@ -215,8 +215,8 @@ require ( ) require ( - cloud.google.com/go/auth v0.9.9 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/auth v0.10.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect dario.cat/mergo v1.0.0 // indirect github.com/DataDog/go-libddwaf/v3 v3.4.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect @@ -250,7 +250,7 @@ require ( ) require ( - cloud.google.com/go/logging v1.11.0 // indirect + cloud.google.com/go/logging v1.12.0 // indirect cloud.google.com/go/longrunning v0.6.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -435,9 +435,9 @@ require ( golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.0 // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect diff --git a/go.sum b/go.sum index 3caa0d8efcb83..808a8caa8449a 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 h1:KHblWIE/KHOwQ6lEbMZt6YpcGve2FEZ1sDtrW1Am5UI= cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= -cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/auth v0.10.0 h1:tWlkvFAh+wwTOzXIjrwM64karR1iTBZ/GRr0S/DULYo= +cloud.google.com/go/auth v0.10.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs= -cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A= +cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= +cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= @@ -1203,8 +1203,8 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= -google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= -google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/api v0.204.0 h1:3PjmQQEDkR/ENVZZwIYB4W/KzYtN8OrqnNcHWpeR8E4= +google.golang.org/api v0.204.0/go.mod h1:69y8QSoKIbL9F94bWgWAq6wGqGwyjBgi2y8rAK8zLag= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= @@ -1212,12 +1212,12 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= -google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= From 3796344d3f592f3c206ed4aca2acb71f4062e50f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:33:50 +0500 Subject: [PATCH 047/223] chore: bump github.com/elastic/go-sysinfo from 1.14.0 to 1.15.0 (#15353) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4ccd0fcbbc455..c5ecf90b95d1c 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( github.com/creack/pty v1.1.21 github.com/dave/dst v0.27.2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/elastic/go-sysinfo v1.14.0 + github.com/elastic/go-sysinfo v1.15.0 github.com/fatih/color v1.18.0 github.com/fatih/structs v1.1.0 github.com/fatih/structtag v1.2.0 diff --git a/go.sum b/go.sum index 808a8caa8449a..78d50a0de64e8 100644 --- a/go.sum +++ b/go.sum @@ -291,8 +291,8 @@ github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8 github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds= github.com/ebitengine/purego v0.6.0-alpha.5 h1:EYID3JOAdmQ4SNZYJHu9V6IqOeRQDBYxqKAg9PyoHFY= github.com/ebitengine/purego v0.6.0-alpha.5/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/elastic/go-sysinfo v1.14.0 h1:dQRtiqLycoOOla7IflZg3aN213vqJmP0lpVpKQ9lUEY= -github.com/elastic/go-sysinfo v1.14.0/go.mod h1:FKUXnZWhnYI0ueO7jhsGV3uQJ5hiz8OqM5b3oGyaRr8= +github.com/elastic/go-sysinfo v1.15.0 h1:54pRFlAYUlVNQ2HbXzLVZlV+fxS7Eax49stzg95M4Xw= +github.com/elastic/go-sysinfo v1.15.0/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk= github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= From cf96d9162535651cc47a4bd3911e520670058f43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:34:35 +0500 Subject: [PATCH 048/223] chore: bump dayjs from 1.11.4 to 1.11.13 in /site (#15332) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- site/package.json | 2 +- site/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/site/package.json b/site/package.json index 799c985ffad2c..e8276b43d4646 100644 --- a/site/package.json +++ b/site/package.json @@ -67,7 +67,7 @@ "cron-parser": "4.9.0", "cronstrue": "2.50.0", "date-fns": "2.30.0", - "dayjs": "1.11.4", + "dayjs": "1.11.13", "emoji-mart": "5.6.0", "file-saver": "2.0.5", "formik": "2.4.6", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 5823a5f5ee144..b0c9eef64d215 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -115,8 +115,8 @@ importers: specifier: 2.30.0 version: 2.30.0 dayjs: - specifier: 1.11.4 - version: 1.11.4 + specifier: 1.11.13 + version: 1.11.13 emoji-mart: specifier: 5.6.0 version: 5.6.0 @@ -3203,8 +3203,8 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} - dayjs@1.11.4: - resolution: {integrity: sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -9204,7 +9204,7 @@ snapshots: dependencies: '@babel/runtime': 7.22.6 - dayjs@1.11.4: {} + dayjs@1.11.13: {} debug@2.6.9: dependencies: From a8caa65124de6dfd8dd42841381c9b265877c25f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:28:05 +1100 Subject: [PATCH 049/223] chore: bump github.com/charmbracelet/lipgloss from 0.13.0 to 1.0.0 (#15354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) from 0.13.0 to 1.0.0.

Release notes

Sourced from github.com/charmbracelet/lipgloss's releases.

v1.0.0

At last: v1.0.0

This is an honorary release indicating that Lip Gloss is now stable. Thank you, open source community, for all your love, support, contributions, and great style.

Stay tuned for a v2 alpha!

v0.13.1

Table improvements, on stream

@​bashbunni went to town in this release and fixed a bunch of bugs, mostly around table. Best of all, she did most of it on stream.

Changelog

Table

Other Stuff

Bonus

New Contributors

Full Changelog: https://github.com/charmbracelet/lipgloss/compare/v0.13.0...v0.13.1


Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

Commits
  • 761d265 feat(deps): bump github.com/charmbracelet/x/ansi from 0.4.0 to 0.4.2
  • 342e7b0 chore(ci): sync golangci-lint config
  • da324b1 feat(deps): bump github.com/charmbracelet/x/ansi from 0.3.2 to 0.4.0
  • 78fd6fd chore(ci): sync golangci-lint config
  • 407dc3d feat(ci): add lint-sync workflow
  • 284c0c5 docs(list): fix list examples in godoc (#404)
  • d858132 fix(table): include margins in cell width (#401)
  • 80b4221 chore(lint): update linter (#405)
  • fa2f4b0 docs(readme): update example screenshot with blend
  • 68ca848 docs: update contributing guidelines (#396)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/charmbracelet/lipgloss&package-manager=go_modules&previous-version=0.13.0&new-version=1.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c5ecf90b95d1c..63ffddab6db7f 100644 --- a/go.mod +++ b/go.mod @@ -203,7 +203,7 @@ require ( github.com/cespare/xxhash v1.1.0 github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.1.0 - github.com/charmbracelet/lipgloss v0.13.0 + github.com/charmbracelet/lipgloss v1.0.0 github.com/coder/serpent v0.8.0 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.21.2 @@ -222,7 +222,7 @@ require ( github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect diff --git a/go.sum b/go.sum index 78d50a0de64e8..fc08a7d7a0fd6 100644 --- a/go.sum +++ b/go.sum @@ -183,10 +183,10 @@ github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69J github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= -github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= -github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= -github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= -github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= +github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= From ffa82659a256e206ab7817618c5f08182af39e35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:52:11 +0500 Subject: [PATCH 050/223] chore: bump the vite group across 1 directory with 2 updates (#15321) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- site/package.json | 4 +- site/pnpm-lock.yaml | 656 ++++++++++++++++++++++---------------------- 2 files changed, 333 insertions(+), 327 deletions(-) diff --git a/site/package.json b/site/package.json index e8276b43d4646..3341749c4bcb7 100644 --- a/site/package.json +++ b/site/package.json @@ -140,7 +140,7 @@ "@types/ssh2": "1.15.1", "@types/ua-parser-js": "0.7.36", "@types/uuid": "9.0.2", - "@vitejs/plugin-react": "4.3.2", + "@vitejs/plugin-react": "4.3.3", "chromatic": "11.16.3", "eventsourcemock": "2.0.0", "express": "4.21.0", @@ -162,7 +162,7 @@ "ts-proto": "1.164.0", "ts-prune": "0.10.3", "typescript": "5.6.3", - "vite": "5.4.8", + "vite": "5.4.10", "vite-plugin-checker": "0.8.0", "vite-plugin-turbosnap": "1.0.3" }, diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index b0c9eef64d215..373a642762fad 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: version: 1.5.1 rollup-plugin-visualizer: specifier: 5.12.0 - version: 5.12.0(rollup@4.24.0) + version: 5.12.0(rollup@4.24.3) semver: specifier: 7.6.2 version: 7.6.2 @@ -252,7 +252,7 @@ importers: version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@storybook/react-vite': specifier: 8.1.11 - version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) + version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) '@storybook/test': specifier: 8.1.11 version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) @@ -329,8 +329,8 @@ importers: specifier: 9.0.2 version: 9.0.2 '@vitejs/plugin-react': - specifier: 4.3.2 - version: 4.3.2(vite@5.4.8(@types/node@20.16.10)) + specifier: 4.3.3 + version: 4.3.3(vite@5.4.10(@types/node@20.16.10)) chromatic: specifier: 11.16.3 version: 11.16.3 @@ -395,11 +395,11 @@ importers: specifier: 5.6.3 version: 5.6.3 vite: - specifier: 5.4.8 - version: 5.4.8(@types/node@20.16.10) + specifier: 5.4.10 + version: 5.4.10(@types/node@20.16.10) vite-plugin-checker: specifier: 0.8.0 - version: 0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) + version: 0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 @@ -431,20 +431,20 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.8': - resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} engines: {node: '>=6.9.0'} - '@babel/core@7.25.8': - resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.25.7': - resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.7': - resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} '@babel/helper-environment-visitor@7.24.7': @@ -463,22 +463,18 @@ packages: resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.25.7': - resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.25.7': - resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.25.7': - resolution: {integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.25.7': - resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} engines: {node: '>=6.9.0'} '@babel/helper-split-export-declaration@7.24.7': @@ -489,8 +485,8 @@ packages: resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.24.7': @@ -505,20 +501,20 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.7': - resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.25.7': - resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.25.7': resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.25.8': - resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -613,14 +609,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.25.7': - resolution: {integrity: sha512-JD9MUnLbPL0WdVK8AWC7F7tTG2OS6u/AKKnsK+NdRhUiVdnzyR1S3kKQCaRLOiaULvUiqK6Z4JQE635VgtCFeg==} + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.25.7': - resolution: {integrity: sha512-S/JXG/KrbIY06iyJPKfxr0qRxnhNOdkNXYBl/rmwgDd72cQLH9tEGkDm/yJPGvcSIUoikzfjMios9i+xT/uv9w==} + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -641,24 +637,24 @@ packages: resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} engines: {node: '>=6.9.0'} - '@babel/template@7.25.7': - resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} '@babel/traverse@7.24.7': resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.7': - resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} engines: {node: '>=6.9.0'} '@babel/types@7.24.7': resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.8': - resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} '@base2/pretty-print-object@1.0.1': @@ -1860,83 +1856,93 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.24.0': - resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} + '@rollup/rollup-android-arm-eabi@4.24.3': + resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.24.0': - resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==} + '@rollup/rollup-android-arm64@4.24.3': + resolution: {integrity: sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.24.0': - resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==} + '@rollup/rollup-darwin-arm64@4.24.3': + resolution: {integrity: sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.24.0': - resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==} + '@rollup/rollup-darwin-x64@4.24.3': + resolution: {integrity: sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': - resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} + '@rollup/rollup-freebsd-arm64@4.24.3': + resolution: {integrity: sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.24.3': + resolution: {integrity: sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.24.3': + resolution: {integrity: sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.24.0': - resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} + '@rollup/rollup-linux-arm-musleabihf@4.24.3': + resolution: {integrity: sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.24.0': - resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} + '@rollup/rollup-linux-arm64-gnu@4.24.3': + resolution: {integrity: sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.24.0': - resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} + '@rollup/rollup-linux-arm64-musl@4.24.3': + resolution: {integrity: sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': - resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.24.3': + resolution: {integrity: sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.24.0': - resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} + '@rollup/rollup-linux-riscv64-gnu@4.24.3': + resolution: {integrity: sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.24.0': - resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} + '@rollup/rollup-linux-s390x-gnu@4.24.3': + resolution: {integrity: sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.24.0': - resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} + '@rollup/rollup-linux-x64-gnu@4.24.3': + resolution: {integrity: sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.24.0': - resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} + '@rollup/rollup-linux-x64-musl@4.24.3': + resolution: {integrity: sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.24.0': - resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} + '@rollup/rollup-win32-arm64-msvc@4.24.3': + resolution: {integrity: sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.24.0': - resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==} + '@rollup/rollup-win32-ia32-msvc@4.24.3': + resolution: {integrity: sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.24.0': - resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==} + '@rollup/rollup-win32-x64-msvc@4.24.3': + resolution: {integrity: sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==} cpu: [x64] os: [win32] @@ -2640,8 +2646,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitejs/plugin-react@4.3.2': - resolution: {integrity: sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==} + '@vitejs/plugin-react@4.3.3': + resolution: {integrity: sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 @@ -2906,8 +2912,8 @@ packages: browser-assert@1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - browserslist@4.24.0: - resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2947,8 +2953,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001668: - resolution: {integrity: sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==} + caniuse-lite@1.0.30001677: + resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==} canvas@3.0.0-rc2: resolution: {integrity: sha512-esx4bYDznnqgRX4G8kaEaf0W3q8xIc51WpmrIitDzmcoEgwnv9wSKdzT6UxWZ4wkVu5+ileofppX0TpyviJRdQ==} @@ -3364,8 +3370,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.36: - resolution: {integrity: sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==} + electron-to-chromium@1.5.50: + resolution: {integrity: sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -5291,8 +5297,8 @@ packages: rollup: optional: true - rollup@4.24.0: - resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} + rollup@4.24.3: + resolution: {integrity: sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5910,8 +5916,8 @@ packages: vite-plugin-turbosnap@1.0.3: resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==} - vite@5.4.8: - resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} + vite@5.4.10: + resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -6121,20 +6127,20 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.25.8': {} + '@babel/compat-data@7.26.2': {} - '@babel/core@7.25.8': + '@babel/core@7.26.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helpers': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/template': 7.25.7 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 convert-source-map: 2.0.0 debug: 4.3.7 gensync: 1.0.0-beta.2 @@ -6143,18 +6149,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.25.7': + '@babel/generator@7.26.2': dependencies: - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.0.2 - '@babel/helper-compilation-targets@7.25.7': + '@babel/helper-compilation-targets@7.25.9': dependencies: - '@babel/compat-data': 7.25.8 - '@babel/helper-validator-option': 7.25.7 - browserslist: 4.24.0 + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 lru-cache: 5.1.1 semver: 7.6.2 @@ -6164,7 +6171,7 @@ snapshots: '@babel/helper-function-name@7.24.7': dependencies: - '@babel/template': 7.25.7 + '@babel/template': 7.25.9 '@babel/types': 7.24.7 '@babel/helper-hoist-variables@7.24.7': @@ -6173,36 +6180,28 @@ snapshots: '@babel/helper-module-imports@7.24.7': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.25.7': + '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.25.7(@babel/core@7.25.8)': + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-imports': 7.25.7 - '@babel/helper-simple-access': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.25.7': {} - - '@babel/helper-simple-access@7.25.7': - dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 - transitivePeerDependencies: - - supports-color + '@babel/helper-plugin-utils@7.25.9': {} '@babel/helper-split-export-declaration@7.24.7': dependencies: @@ -6210,7 +6209,7 @@ snapshots: '@babel/helper-string-parser@7.24.7': {} - '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.24.7': {} @@ -6218,12 +6217,12 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-option@7.25.7': {} + '@babel/helper-validator-option@7.25.9': {} - '@babel/helpers@7.25.7': + '@babel/helpers@7.26.0': dependencies: - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 '@babel/highlight@7.25.7': dependencies: @@ -6232,104 +6231,104 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.0 - '@babel/parser@7.25.8': + '@babel/parser@7.26.2': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.8)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.8)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.8)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.8)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-react-jsx-self@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-react-jsx-source@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 '@babel/runtime@7.22.6': dependencies: @@ -6347,34 +6346,34 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.25.7': + '@babel/template@7.25.9': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@babel/traverse@7.24.7': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-function-name': 7.24.7 '@babel/helper-hoist-variables': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.25.8 + '@babel/parser': 7.26.2 '@babel/types': 7.24.7 debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/traverse@7.25.7': + '@babel/traverse@7.25.9': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: @@ -6386,11 +6385,10 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - '@babel/types@7.25.8': + '@babel/types@7.26.0': dependencies: - '@babel/helper-string-parser': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - to-fast-properties: 2.0.0 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 '@base2/pretty-print-object@1.0.1': {} @@ -7051,7 +7049,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -7087,13 +7085,13 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.6.3) - vite: 5.4.8(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.16.10) optionalDependencies: typescript: 5.6.3 @@ -7504,60 +7502,66 @@ snapshots: '@remix-run/router@1.19.2': {} - '@rollup/pluginutils@5.0.5(rollup@4.24.0)': + '@rollup/pluginutils@5.0.5(rollup@4.24.3)': dependencies: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.24.0 + rollup: 4.24.3 + + '@rollup/rollup-android-arm-eabi@4.24.3': + optional: true - '@rollup/rollup-android-arm-eabi@4.24.0': + '@rollup/rollup-android-arm64@4.24.3': optional: true - '@rollup/rollup-android-arm64@4.24.0': + '@rollup/rollup-darwin-arm64@4.24.3': optional: true - '@rollup/rollup-darwin-arm64@4.24.0': + '@rollup/rollup-darwin-x64@4.24.3': optional: true - '@rollup/rollup-darwin-x64@4.24.0': + '@rollup/rollup-freebsd-arm64@4.24.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': + '@rollup/rollup-freebsd-x64@4.24.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.24.0': + '@rollup/rollup-linux-arm-gnueabihf@4.24.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.24.0': + '@rollup/rollup-linux-arm-musleabihf@4.24.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.24.0': + '@rollup/rollup-linux-arm64-gnu@4.24.3': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': + '@rollup/rollup-linux-arm64-musl@4.24.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.24.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.24.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.24.0': + '@rollup/rollup-linux-riscv64-gnu@4.24.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.24.0': + '@rollup/rollup-linux-s390x-gnu@4.24.3': optional: true - '@rollup/rollup-linux-x64-musl@4.24.0': + '@rollup/rollup-linux-x64-gnu@4.24.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.24.0': + '@rollup/rollup-linux-x64-musl@4.24.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.24.0': + '@rollup/rollup-win32-arm64-msvc@4.24.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.24.0': + '@rollup/rollup-win32-ia32-msvc@4.24.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.24.3': optional: true '@sinclair/typebox@0.27.8': {} @@ -7611,7 +7615,7 @@ snapshots: '@storybook/addon-docs@8.1.11(@types/react-dom@18.3.1)(prettier@3.3.3)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@mdx-js/react': 3.0.1(@types/react@18.3.12)(react@18.3.1) '@storybook/blocks': 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/client-logger': 8.1.11 @@ -7791,7 +7795,7 @@ snapshots: - prettier - supports-color - '@storybook/builder-vite@8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10))': + '@storybook/builder-vite@8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10))': dependencies: '@storybook/channels': 8.1.11 '@storybook/client-logger': 8.1.11 @@ -7810,7 +7814,7 @@ snapshots: fs-extra: 11.2.0 magic-string: 0.30.5 ts-dedent: 2.2.0 - vite: 5.4.8(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.16.10) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: @@ -7934,10 +7938,10 @@ snapshots: '@storybook/csf-tools@8.1.11': dependencies: - '@babel/generator': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 '@storybook/csf': 0.1.9 '@storybook/types': 8.1.11 fs-extra: 11.2.0 @@ -8037,11 +8041,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-vite@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10))': + '@storybook/react-vite@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) - '@rollup/pluginutils': 5.0.5(rollup@4.24.0) - '@storybook/builder-vite': 8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) + '@rollup/pluginutils': 5.0.5(rollup@4.24.3) + '@storybook/builder-vite': 8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) '@storybook/node-logger': 8.1.11 '@storybook/react': 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@storybook/types': 8.1.11 @@ -8052,7 +8056,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 tsconfig-paths: 4.2.0 - vite: 5.4.8(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.16.10) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -8332,20 +8336,20 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__traverse@7.20.4': dependencies: @@ -8353,7 +8357,7 @@ snapshots: '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 '@types/body-parser@1.19.2': dependencies: @@ -8608,14 +8612,14 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@20.16.10))': + '@vitejs/plugin-react@4.3.3(vite@5.4.10(@types/node@20.16.10))': dependencies: - '@babel/core': 7.25.8 - '@babel/plugin-transform-react-jsx-self': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-react-jsx-source': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.8(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.16.10) transitivePeerDependencies: - supports-color @@ -8814,13 +8818,13 @@ snapshots: transitivePeerDependencies: - debug - babel-jest@29.7.0(@babel/core@7.25.8): + babel-jest@29.7.0(@babel/core@7.26.0): dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.25.8) + babel-preset-jest: 29.6.3(@babel/core@7.26.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -8829,7 +8833,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.25.9 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -8839,8 +8843,8 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 @@ -8850,30 +8854,30 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.8 - babel-preset-current-node-syntax@1.1.0(@babel/core@7.25.8): - dependencies: - '@babel/core': 7.25.8 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.8) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.8) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.8) - '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.25.8) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.8) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.8) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.8) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.8) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.8) - - babel-preset-jest@29.6.3(@babel/core@7.25.8): - dependencies: - '@babel/core': 7.25.8 + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@29.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.8) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) bail@2.0.2: {} @@ -8929,12 +8933,12 @@ snapshots: browser-assert@1.2.1: {} - browserslist@4.24.0: + browserslist@4.24.2: dependencies: - caniuse-lite: 1.0.30001668 - electron-to-chromium: 1.5.36 + caniuse-lite: 1.0.30001677 + electron-to-chromium: 1.5.50 node-releases: 2.0.18 - update-browserslist-db: 1.1.1(browserslist@4.24.0) + update-browserslist-db: 1.1.1(browserslist@4.24.2) bser@2.1.1: dependencies: @@ -8972,7 +8976,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001668: {} + caniuse-lite@1.0.30001677: {} canvas@3.0.0-rc2: dependencies: @@ -9341,7 +9345,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.36: {} + electron-to-chromium@1.5.50: {} emittery@0.13.1: {} @@ -10200,8 +10204,8 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.25.8 - '@babel/parser': 7.25.8 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.2 @@ -10210,8 +10214,8 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.25.8 - '@babel/parser': 7.25.8 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.2 @@ -10301,10 +10305,10 @@ snapshots: jest-config@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.25.8) + babel-jest: 29.7.0(@babel/core@7.26.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -10421,7 +10425,7 @@ snapshots: jest-message-util@29.6.2: dependencies: - '@babel/code-frame': 7.25.7 + '@babel/code-frame': 7.26.2 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -10535,15 +10539,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.25.8 - '@babel/generator': 7.25.7 - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.8) - '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.25.8) - '@babel/types': 7.25.8 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.26.0) + '@babel/types': 7.26.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.8) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -11319,7 +11323,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.25.7 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -11384,7 +11388,7 @@ snapshots: postcss@8.4.47: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 prebuild-install@7.1.2: @@ -11554,7 +11558,7 @@ snapshots: react-docgen@7.0.3: dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 '@types/babel__core': 7.20.5 @@ -11848,35 +11852,37 @@ snapshots: glob: 7.2.3 optional: true - rollup-plugin-visualizer@5.12.0(rollup@4.24.0): + rollup-plugin-visualizer@5.12.0(rollup@4.24.3): dependencies: open: 8.4.2 picomatch: 2.3.1 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: - rollup: 4.24.0 + rollup: 4.24.3 - rollup@4.24.0: + rollup@4.24.3: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.24.0 - '@rollup/rollup-android-arm64': 4.24.0 - '@rollup/rollup-darwin-arm64': 4.24.0 - '@rollup/rollup-darwin-x64': 4.24.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.24.0 - '@rollup/rollup-linux-arm-musleabihf': 4.24.0 - '@rollup/rollup-linux-arm64-gnu': 4.24.0 - '@rollup/rollup-linux-arm64-musl': 4.24.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.24.0 - '@rollup/rollup-linux-riscv64-gnu': 4.24.0 - '@rollup/rollup-linux-s390x-gnu': 4.24.0 - '@rollup/rollup-linux-x64-gnu': 4.24.0 - '@rollup/rollup-linux-x64-musl': 4.24.0 - '@rollup/rollup-win32-arm64-msvc': 4.24.0 - '@rollup/rollup-win32-ia32-msvc': 4.24.0 - '@rollup/rollup-win32-x64-msvc': 4.24.0 + '@rollup/rollup-android-arm-eabi': 4.24.3 + '@rollup/rollup-android-arm64': 4.24.3 + '@rollup/rollup-darwin-arm64': 4.24.3 + '@rollup/rollup-darwin-x64': 4.24.3 + '@rollup/rollup-freebsd-arm64': 4.24.3 + '@rollup/rollup-freebsd-x64': 4.24.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.3 + '@rollup/rollup-linux-arm-musleabihf': 4.24.3 + '@rollup/rollup-linux-arm64-gnu': 4.24.3 + '@rollup/rollup-linux-arm64-musl': 4.24.3 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.3 + '@rollup/rollup-linux-riscv64-gnu': 4.24.3 + '@rollup/rollup-linux-s390x-gnu': 4.24.3 + '@rollup/rollup-linux-x64-gnu': 4.24.3 + '@rollup/rollup-linux-x64-musl': 4.24.3 + '@rollup/rollup-win32-arm64-msvc': 4.24.3 + '@rollup/rollup-win32-ia32-msvc': 4.24.3 + '@rollup/rollup-win32-x64-msvc': 4.24.3 fsevents: 2.3.3 run-async@3.0.0: {} @@ -12415,9 +12421,9 @@ snapshots: webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 - update-browserslist-db@1.1.1(browserslist@4.24.0): + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -12485,7 +12491,7 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-plugin-checker@0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.8(@types/node@20.16.10)): + vite-plugin-checker@0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)): dependencies: '@babel/code-frame': 7.25.7 ansi-escapes: 4.3.2 @@ -12497,7 +12503,7 @@ snapshots: npm-run-path: 4.0.1 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - vite: 5.4.8(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.16.10) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.12 @@ -12510,11 +12516,11 @@ snapshots: vite-plugin-turbosnap@1.0.3: {} - vite@5.4.8(@types/node@20.16.10): + vite@5.4.10(@types/node@20.16.10): dependencies: esbuild: 0.21.5 postcss: 8.4.47 - rollup: 4.24.0 + rollup: 4.24.3 optionalDependencies: '@types/node': 20.16.10 fsevents: 2.3.3 From 1c299448623cac7b824acbc2bc6c57c81ca51513 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 4 Nov 2024 07:19:23 -0800 Subject: [PATCH 051/223] chore: update link to licensed features in README.md (#15362) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b629891297d8..395534c44dd9c 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Browse our docs [here](https://coder.com/docs) or visit a specific section below - [**Workspaces**](https://coder.com/docs/workspaces): Workspaces contain the IDEs, dependencies, and configuration information needed for software development - [**IDEs**](https://coder.com/docs/ides): Connect your existing editor to a workspace - [**Administration**](https://coder.com/docs/admin): Learn how to operate Coder -- [**Enterprise**](https://coder.com/docs/enterprise): Learn about our paid features built for large teams +- [**Premium**](https://coder.com/pricing#compare-plans): Learn about our paid features built for large teams ## Support From 1bfa7d42e80a95a6c337253369879c6dcfbb8b00 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 4 Nov 2024 17:23:31 +0100 Subject: [PATCH 052/223] chore: add postgres template caching for tests (#15336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is the first in a series aimed at closing [#15109](https://github.com/coder/coder/issues/15109). ### Changes - **Template Database Creation:** `dbtestutil.Open` now has the ability to create a template database if none is provided via `DB_FROM`. The template database’s name is derived from a hash of the migration files, ensuring that it can be reused across tests and is automatically updated whenever migrations change. - **Optimized Database Handling:** Previously, `dbtestutil.Open` would spin up a new container for each test when `DB_FROM` was unset. Now, it first checks for an active PostgreSQL instance on `localhost:5432`. If none is found, it creates a single container that remains available for subsequent tests, eliminating repeated container startups. These changes address the long individual test times (10+ seconds) reported by some users, likely due to the time Docker took to start and complete migrations. --- Makefile | 2 +- cli/resetpassword_test.go | 3 +- cli/server_createadminuser_test.go | 12 +- cli/server_test.go | 6 +- coderd/database/db_test.go | 3 +- coderd/database/dbtestutil/db.go | 15 +- coderd/database/dbtestutil/postgres.go | 529 ++++++++++++++++---- coderd/database/dbtestutil/postgres_test.go | 87 +++- coderd/database/gen/dump/main.go | 27 +- coderd/database/migrations/migrate.go | 55 ++ coderd/database/migrations/migrate_test.go | 3 +- coderd/database/pubsub/pubsub_linux_test.go | 19 +- coderd/database/pubsub/pubsub_test.go | 6 +- enterprise/cli/server_dbcrypt_test.go | 3 +- enterprise/tailnet/pgcoord_test.go | 3 +- 15 files changed, 629 insertions(+), 144 deletions(-) diff --git a/Makefile b/Makefile index 084e8bb77e5f0..fe6e527fe712e 100644 --- a/Makefile +++ b/Makefile @@ -765,7 +765,7 @@ sqlc-vet: test-postgres-docker test-postgres: test-postgres-docker # The postgres test is prone to failure, so we limit parallelism for # more consistent execution. - $(GIT_FLAGS) DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \ + $(GIT_FLAGS) DB=ci gotestsum \ --junitfile="gotests.xml" \ --jsonfile="gotests.json" \ --packages="./..." -- \ diff --git a/cli/resetpassword_test.go b/cli/resetpassword_test.go index 0cd90f5b4cd00..de712874f3f07 100644 --- a/cli/resetpassword_test.go +++ b/cli/resetpassword_test.go @@ -32,9 +32,8 @@ func TestResetPassword(t *testing.T) { const newPassword = "MyNewPassword!" // start postgres and coder server processes - connectionURL, closeFunc, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closeFunc() ctx, cancelFunc := context.WithCancel(context.Background()) serverDone := make(chan struct{}) serverinv, cfg := clitest.New(t, diff --git a/cli/server_createadminuser_test.go b/cli/server_createadminuser_test.go index 17c02b6548c09..7660d71e89d99 100644 --- a/cli/server_createadminuser_test.go +++ b/cli/server_createadminuser_test.go @@ -85,9 +85,8 @@ func TestServerCreateAdminUser(t *testing.T) { // Skip on non-Linux because it spawns a PostgreSQL instance. t.SkipNow() } - connectionURL, closeFunc, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closeFunc() sqlDB, err := sql.Open("postgres", connectionURL) require.NoError(t, err) @@ -151,9 +150,8 @@ func TestServerCreateAdminUser(t *testing.T) { // Skip on non-Linux because it spawns a PostgreSQL instance. t.SkipNow() } - connectionURL, closeFunc, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closeFunc() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) defer cancel() @@ -185,9 +183,8 @@ func TestServerCreateAdminUser(t *testing.T) { // Skip on non-Linux because it spawns a PostgreSQL instance. t.SkipNow() } - connectionURL, closeFunc, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closeFunc() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) defer cancel() @@ -225,9 +222,8 @@ func TestServerCreateAdminUser(t *testing.T) { // Skip on non-Linux because it spawns a PostgreSQL instance. t.SkipNow() } - connectionURL, closeFunc, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closeFunc() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() diff --git a/cli/server_test.go b/cli/server_test.go index ad6a98038c7bb..83a7f7171c6f5 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1598,9 +1598,8 @@ func TestServer_Production(t *testing.T) { // Skip on non-Linux because it spawns a PostgreSQL instance. t.SkipNow() } - connectionURL, closeFunc, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closeFunc() // Postgres + race detector + CI = slow. ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitSuperLong*3) @@ -1803,9 +1802,8 @@ func TestConnectToPostgres(t *testing.T) { log := slogtest.Make(t, nil) - dbURL, closeFunc, err := dbtestutil.Open() + dbURL, err := dbtestutil.Open(t) require.NoError(t, err) - t.Cleanup(closeFunc) sqlDB, err := cli.ConnectToPostgres(ctx, log, "postgres", dbURL) require.NoError(t, err) diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go index a6df18fcbb8c8..b4580527c843a 100644 --- a/coderd/database/db_test.go +++ b/coderd/database/db_test.go @@ -87,9 +87,8 @@ func TestNestedInTx(t *testing.T) { func testSQLDB(t testing.TB) *sql.DB { t.Helper() - connection, closeFn, err := dbtestutil.Open() + connection, err := dbtestutil.Open(t) require.NoError(t, err) - t.Cleanup(closeFn) db, err := sql.Open("postgres", connection) require.NoError(t, err) diff --git a/coderd/database/dbtestutil/db.go b/coderd/database/dbtestutil/db.go index bc8c571795629..85784be9b9a6c 100644 --- a/coderd/database/dbtestutil/db.go +++ b/coderd/database/dbtestutil/db.go @@ -95,21 +95,17 @@ func NewDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub) { opt(&o) } - db := dbmem.New() - ps := pubsub.NewInMemory() + var db database.Store + var ps pubsub.Pubsub if WillUsePostgres() { connectionURL := os.Getenv("CODER_PG_CONNECTION_URL") if connectionURL == "" && o.url != "" { connectionURL = o.url } if connectionURL == "" { - var ( - err error - closePg func() - ) - connectionURL, closePg, err = Open() + var err error + connectionURL, err = Open(t) require.NoError(t, err) - t.Cleanup(closePg) } if o.fixedTimezone == "" { @@ -143,6 +139,9 @@ func NewDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub) { t.Cleanup(func() { _ = ps.Close() }) + } else { + db = dbmem.New() + ps = pubsub.NewInMemory() } return db, ps diff --git a/coderd/database/dbtestutil/postgres.go b/coderd/database/dbtestutil/postgres.go index 3a559778b6968..a58ffb570763f 100644 --- a/coderd/database/dbtestutil/postgres.go +++ b/coderd/database/dbtestutil/postgres.go @@ -1,134 +1,498 @@ package dbtestutil import ( + "context" + "crypto/sha256" "database/sql" + "encoding/hex" + "errors" "fmt" + "net" "os" + "path/filepath" "strconv" + "strings" + "sync" "time" "github.com/cenkalti/backoff/v4" + "github.com/gofrs/flock" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database/migrations" "github.com/coder/coder/v2/cryptorand" + "github.com/coder/retry" ) -// Open creates a new PostgreSQL database instance. With DB_FROM environment variable set, it clones a database -// from the provided template. With the environment variable unset, it creates a new Docker container running postgres. -func Open() (string, func(), error) { - if os.Getenv("DB_FROM") != "" { - // In CI, creating a Docker container for each test is slow. - // This expects a PostgreSQL instance with the hardcoded credentials - // available. - dbURL := "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable" - db, err := sql.Open("postgres", dbURL) +type ConnectionParams struct { + Username string + Password string + Host string + Port string + DBName string +} + +func (p ConnectionParams) DSN() string { + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", p.Username, p.Password, p.Host, p.Port, p.DBName) +} + +// These variables are global because all tests share them. +var ( + connectionParamsInitOnce sync.Once + defaultConnectionParams ConnectionParams + errDefaultConnectionParamsInit error +) + +// initDefaultConnection initializes the default postgres connection parameters. +// It first checks if the database is running at localhost:5432. If it is, it will +// use that database. If it's not, it will start a new container and use that. +func initDefaultConnection(t TBSubset) error { + params := ConnectionParams{ + Username: "postgres", + Password: "postgres", + Host: "127.0.0.1", + Port: "5432", + DBName: "postgres", + } + dsn := params.DSN() + db, dbErr := sql.Open("postgres", dsn) + if dbErr == nil { + dbErr = db.Ping() + if closeErr := db.Close(); closeErr != nil { + return xerrors.Errorf("close db: %w", closeErr) + } + } + shouldOpenContainer := false + if dbErr != nil { + errSubstrings := []string{ + "connection refused", // this happens on Linux when there's nothing listening on the port + "No connection could be made", // like above but Windows + } + errString := dbErr.Error() + for _, errSubstring := range errSubstrings { + if strings.Contains(errString, errSubstring) { + shouldOpenContainer = true + break + } + } + } + if dbErr != nil && shouldOpenContainer { + // If there's no database running on the default port, we'll start a + // postgres container. We won't be cleaning it up so it can be reused + // by subsequent tests. It'll keep on running until the user terminates + // it manually. + container, _, err := openContainer(t, DBContainerOptions{ + Name: "coder-test-postgres", + Port: 5432, + }) if err != nil { - return "", nil, xerrors.Errorf("connect to ci postgres: %w", err) + return xerrors.Errorf("open container: %w", err) } + params.Host = container.Host + params.Port = container.Port + dsn = params.DSN() - defer db.Close() + // Retry connecting for at most 10 seconds. + // The fact that openContainer succeeded does not + // mean that port forwarding is ready. + for r := retry.New(100*time.Millisecond, 10*time.Second); r.Wait(context.Background()); { + db, connErr := sql.Open("postgres", dsn) + if connErr == nil { + connErr = db.Ping() + if closeErr := db.Close(); closeErr != nil { + return xerrors.Errorf("close db, container: %w", closeErr) + } + } + if connErr == nil { + break + } + } + } else if dbErr != nil { + return xerrors.Errorf("open postgres connection: %w", dbErr) + } + defaultConnectionParams = params + return nil +} + +type OpenOptions struct { + DBFrom *string +} + +type OpenOption func(*OpenOptions) + +// WithDBFrom sets the template database to use when creating a new database. +// Overrides the DB_FROM environment variable. +func WithDBFrom(dbFrom string) OpenOption { + return func(o *OpenOptions) { + o.DBFrom = &dbFrom + } +} + +// TBSubset is a subset of the testing.TB interface. +// It allows to use dbtestutil.Open outside of tests. +type TBSubset interface { + Cleanup(func()) + Helper() + Logf(format string, args ...any) +} + +// Open creates a new PostgreSQL database instance. +// If there's a database running at localhost:5432, it will use that. +// Otherwise, it will start a new postgres container. +func Open(t TBSubset, opts ...OpenOption) (string, error) { + t.Helper() - dbName, err := cryptorand.StringCharset(cryptorand.Lower, 10) + connectionParamsInitOnce.Do(func() { + errDefaultConnectionParamsInit = initDefaultConnection(t) + }) + if errDefaultConnectionParamsInit != nil { + return "", xerrors.Errorf("init default connection params: %w", errDefaultConnectionParamsInit) + } + + openOptions := OpenOptions{} + for _, opt := range opts { + opt(&openOptions) + } + + var ( + username = defaultConnectionParams.Username + password = defaultConnectionParams.Password + host = defaultConnectionParams.Host + port = defaultConnectionParams.Port + ) + + // Use a time-based prefix to make it easier to find the database + // when debugging. + now := time.Now().Format("test_2006_01_02_15_04_05") + dbSuffix, err := cryptorand.StringCharset(cryptorand.Lower, 10) + if err != nil { + return "", xerrors.Errorf("generate db suffix: %w", err) + } + dbName := now + "_" + dbSuffix + + // if empty createDatabaseFromTemplate will create a new template db + templateDBName := os.Getenv("DB_FROM") + if openOptions.DBFrom != nil { + templateDBName = *openOptions.DBFrom + } + if err = createDatabaseFromTemplate(t, defaultConnectionParams, dbName, templateDBName); err != nil { + return "", xerrors.Errorf("create database: %w", err) + } + + t.Cleanup(func() { + cleanupDbURL := defaultConnectionParams.DSN() + cleanupConn, err := sql.Open("postgres", cleanupDbURL) if err != nil { - return "", nil, xerrors.Errorf("generate db name: %w", err) + t.Logf("cleanup database %q: failed to connect to postgres: %s\n", dbName, err.Error()) + return } - - dbName = "ci" + dbName - _, err = db.Exec("CREATE DATABASE " + dbName + " WITH TEMPLATE " + os.Getenv("DB_FROM")) + defer func() { + if err := cleanupConn.Close(); err != nil { + t.Logf("cleanup database %q: failed to close connection: %s\n", dbName, err.Error()) + } + }() + _, err = cleanupConn.Exec("DROP DATABASE " + dbName + ";") if err != nil { - return "", nil, xerrors.Errorf("create db with template: %w", err) + t.Logf("failed to clean up database %q: %s\n", dbName, err.Error()) + return } + }) - dsn := "postgres://postgres:postgres@127.0.0.1:5432/" + dbName + "?sslmode=disable" - // Normally this would get cleaned up by removing the container but if we - // reuse the same container for multiple tests we run the risk of filling - // up our disk. Avoid this! - cleanup := func() { - cleanupConn, err := sql.Open("postgres", dbURL) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "cleanup database %q: failed to connect to postgres: %s\n", dbName, err.Error()) - } - defer cleanupConn.Close() - _, err = cleanupConn.Exec("DROP DATABASE " + dbName + ";") - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "failed to clean up database %q: %s\n", dbName, err.Error()) + dsn := ConnectionParams{ + Username: username, + Password: password, + Host: host, + Port: port, + DBName: dbName, + }.DSN() + return dsn, nil +} + +// createDatabaseFromTemplate creates a new database from a template database. +// If templateDBName is empty, it will create a new template database based on +// the current migrations, and name it "tpl_". Or if it's +// already been created, it will use that. +func createDatabaseFromTemplate(t TBSubset, connParams ConnectionParams, newDBName string, templateDBName string) error { + t.Helper() + + dbURL := connParams.DSN() + db, err := sql.Open("postgres", dbURL) + if err != nil { + return xerrors.Errorf("connect to postgres: %w", err) + } + defer func() { + if err := db.Close(); err != nil { + t.Logf("create database from template: failed to close connection: %s\n", err.Error()) + } + }() + + emptyTemplateDBName := templateDBName == "" + if emptyTemplateDBName { + templateDBName = fmt.Sprintf("tpl_%s", migrations.GetMigrationsHash()[:32]) + } + _, err = db.Exec("CREATE DATABASE " + newDBName + " WITH TEMPLATE " + templateDBName) + if err == nil { + // Template database already exists and we successfully created the new database. + return nil + } + tplDbDoesNotExistOccurred := strings.Contains(err.Error(), "template database") && strings.Contains(err.Error(), "does not exist") + if (tplDbDoesNotExistOccurred && !emptyTemplateDBName) || !tplDbDoesNotExistOccurred { + // First and case: user passed a templateDBName that doesn't exist. + // Second and case: some other error. + return xerrors.Errorf("create db with template: %w", err) + } + if !emptyTemplateDBName { + // sanity check + panic("templateDBName is not empty. there's a bug in the code above") + } + // The templateDBName is empty, so we need to create the template database. + // We will use a tx to obtain a lock, so another test or process doesn't race with us. + tx, err := db.BeginTx(context.Background(), nil) + if err != nil { + return xerrors.Errorf("begin tx: %w", err) + } + defer func() { + err := tx.Rollback() + if err != nil && !errors.Is(err, sql.ErrTxDone) { + t.Logf("create database from template: failed to rollback tx: %s\n", err.Error()) + } + }() + // 2137 is an arbitrary number. We just need a lock that is unique to creating + // the template database. + _, err = tx.Exec("SELECT pg_advisory_xact_lock(2137)") + if err != nil { + return xerrors.Errorf("acquire lock: %w", err) + } + + // Someone else might have created the template db while we were waiting. + tplDbExistsRes, err := tx.Query("SELECT 1 FROM pg_database WHERE datname = $1", templateDBName) + if err != nil { + return xerrors.Errorf("check if db exists: %w", err) + } + tplDbAlreadyExists := tplDbExistsRes.Next() + if err := tplDbExistsRes.Close(); err != nil { + return xerrors.Errorf("close tpl db exists res: %w", err) + } + if !tplDbAlreadyExists { + // We will use a temporary template database to avoid race conditions. We will + // rename it to the real template database name after we're sure it was fully + // initialized. + // It's dropped here to ensure that if a previous run of this function failed + // midway, we don't encounter issues with the temporary database still existing. + tmpTemplateDBName := "tmp_" + templateDBName + // We're using db instead of tx here because you can't run `DROP DATABASE` inside + // a transaction. + if _, err := db.Exec("DROP DATABASE IF EXISTS " + tmpTemplateDBName); err != nil { + return xerrors.Errorf("drop tmp template db: %w", err) + } + if _, err := db.Exec("CREATE DATABASE " + tmpTemplateDBName); err != nil { + return xerrors.Errorf("create tmp template db: %w", err) + } + tplDbURL := ConnectionParams{ + Username: connParams.Username, + Password: connParams.Password, + Host: connParams.Host, + Port: connParams.Port, + DBName: tmpTemplateDBName, + }.DSN() + tplDb, err := sql.Open("postgres", tplDbURL) + if err != nil { + return xerrors.Errorf("connect to template db: %w", err) + } + defer func() { + if err := tplDb.Close(); err != nil { + t.Logf("create database from template: failed to close template db: %s\n", err.Error()) } + }() + if err := migrations.Up(tplDb); err != nil { + return xerrors.Errorf("migrate template db: %w", err) } - return dsn, cleanup, nil + if err := tplDb.Close(); err != nil { + return xerrors.Errorf("close template db: %w", err) + } + if _, err := db.Exec("ALTER DATABASE " + tmpTemplateDBName + " RENAME TO " + templateDBName); err != nil { + return xerrors.Errorf("rename tmp template db: %w", err) + } + } + + // Try to create the database again now that a template exists. + if _, err = db.Exec("CREATE DATABASE " + newDBName + " WITH TEMPLATE " + templateDBName); err != nil { + return xerrors.Errorf("create db with template after migrations: %w", err) } - return OpenContainerized(0) + if err = tx.Commit(); err != nil { + return xerrors.Errorf("commit tx: %w", err) + } + return nil } -// OpenContainerized creates a new PostgreSQL server using a Docker container. If port is nonzero, forward host traffic -// to that port to the database. If port is zero, allocate a free port from the OS. -func OpenContainerized(port int) (string, func(), error) { +type DBContainerOptions struct { + Port int + Name string +} + +type container struct { + Resource *dockertest.Resource + Pool *dockertest.Pool + Host string + Port string +} + +// OpenContainer creates a new PostgreSQL server using a Docker container. If port is nonzero, forward host traffic +// to that port to the database. If port is zero, allocate a free port from the OS. +// If name is set, we'll ensure that only one container is started with that name. If it's already running, we'll use that. +// Otherwise, we'll start a new container. +func openContainer(t TBSubset, opts DBContainerOptions) (container, func(), error) { + if opts.Name != "" { + // We only want to start the container once per unique name, + // so we take an inter-process lock to avoid concurrent test runs + // racing with us. + nameHash := sha256.Sum256([]byte(opts.Name)) + nameHashStr := hex.EncodeToString(nameHash[:]) + lock := flock.New(filepath.Join(os.TempDir(), "coder-postgres-container-"+nameHashStr[:8])) + if err := lock.Lock(); err != nil { + return container{}, nil, xerrors.Errorf("lock: %w", err) + } + defer func() { + err := lock.Unlock() + if err != nil { + t.Logf("create database from template: failed to unlock: %s\n", err.Error()) + } + }() + } + pool, err := dockertest.NewPool("") if err != nil { - return "", nil, xerrors.Errorf("create pool: %w", err) + return container{}, nil, xerrors.Errorf("create pool: %w", err) + } + + var resource *dockertest.Resource + var tempDir string + if opts.Name != "" { + // If the container already exists, we'll use it. + resource, _ = pool.ContainerByName(opts.Name) + } + if resource == nil { + tempDir, err = os.MkdirTemp(os.TempDir(), "postgres") + if err != nil { + return container{}, nil, xerrors.Errorf("create tempdir: %w", err) + } + runOptions := dockertest.RunOptions{ + Repository: "gcr.io/coder-dev-1/postgres", + Tag: "13", + Env: []string{ + "POSTGRES_PASSWORD=postgres", + "POSTGRES_USER=postgres", + "POSTGRES_DB=postgres", + // The location for temporary database files! + "PGDATA=/tmp", + "listen_addresses = '*'", + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "5432/tcp": {{ + // Manually specifying a host IP tells Docker just to use an IPV4 address. + // If we don't do this, we hit a fun bug: + // https://github.com/moby/moby/issues/42442 + // where the ipv4 and ipv6 ports might be _different_ and collide with other running docker containers. + HostIP: "0.0.0.0", + HostPort: strconv.FormatInt(int64(opts.Port), 10), + }}, + }, + Mounts: []string{ + // The postgres image has a VOLUME parameter in it's image. + // If we don't mount at this point, Docker will allocate a + // volume for this directory. + // + // This isn't used anyways, since we override PGDATA. + fmt.Sprintf("%s:/var/lib/postgresql/data", tempDir), + }, + Cmd: []string{"-c", "max_connections=1000"}, + } + if opts.Name != "" { + runOptions.Name = opts.Name + } + resource, err = pool.RunWithOptions(&runOptions, func(config *docker.HostConfig) { + // set AutoRemove to true so that stopped container goes away by itself + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + config.Tmpfs = map[string]string{ + "/tmp": "rw", + } + }) + if err != nil { + return container{}, nil, xerrors.Errorf("could not start resource: %w", err) + } } - tempDir, err := os.MkdirTemp(os.TempDir(), "postgres") + hostAndPort := resource.GetHostPort("5432/tcp") + host, port, err := net.SplitHostPort(hostAndPort) if err != nil { - return "", nil, xerrors.Errorf("create tempdir: %w", err) - } - - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "gcr.io/coder-dev-1/postgres", - Tag: "13", - Env: []string{ - "POSTGRES_PASSWORD=postgres", - "POSTGRES_USER=postgres", - "POSTGRES_DB=postgres", - // The location for temporary database files! - "PGDATA=/tmp", - "listen_addresses = '*'", - }, - PortBindings: map[docker.Port][]docker.PortBinding{ - "5432/tcp": {{ - // Manually specifying a host IP tells Docker just to use an IPV4 address. - // If we don't do this, we hit a fun bug: - // https://github.com/moby/moby/issues/42442 - // where the ipv4 and ipv6 ports might be _different_ and collide with other running docker containers. - HostIP: "0.0.0.0", - HostPort: strconv.FormatInt(int64(port), 10), - }}, - }, - Mounts: []string{ - // The postgres image has a VOLUME parameter in it's image. - // If we don't mount at this point, Docker will allocate a - // volume for this directory. - // - // This isn't used anyways, since we override PGDATA. - fmt.Sprintf("%s:/var/lib/postgresql/data", tempDir), - }, - }, func(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) + return container{}, nil, xerrors.Errorf("split host and port: %w", err) + } + + for r := retry.New(50*time.Millisecond, 15*time.Second); r.Wait(context.Background()); { + stdout := &strings.Builder{} + stderr := &strings.Builder{} + _, err = resource.Exec([]string{"pg_isready", "-h", "127.0.0.1"}, dockertest.ExecOptions{ + StdOut: stdout, + StdErr: stderr, + }) + if err == nil { + break + } + } if err != nil { - return "", nil, xerrors.Errorf("could not start resource: %w", err) + return container{}, nil, xerrors.Errorf("pg_isready: %w", err) } - hostAndPort := resource.GetHostPort("5432/tcp") - dbURL := fmt.Sprintf("postgres://postgres:postgres@%s/postgres?sslmode=disable", hostAndPort) + return container{ + Host: host, + Port: port, + Resource: resource, + Pool: pool, + }, func() { + _ = pool.Purge(resource) + if tempDir != "" { + _ = os.RemoveAll(tempDir) + } + }, nil +} + +// OpenContainerized creates a new PostgreSQL server using a Docker container. If port is nonzero, forward host traffic +// to that port to the database. If port is zero, allocate a free port from the OS. +// The user is responsible for calling the returned cleanup function. +func OpenContainerized(t TBSubset, opts DBContainerOptions) (string, func(), error) { + container, containerCleanup, err := openContainer(t, opts) + defer func() { + if err != nil { + containerCleanup() + } + }() + if err != nil { + return "", nil, xerrors.Errorf("open container: %w", err) + } + dbURL := ConnectionParams{ + Username: "postgres", + Password: "postgres", + Host: container.Host, + Port: container.Port, + DBName: "postgres", + }.DSN() // Docker should hard-kill the container after 120 seconds. - err = resource.Expire(120) + err = container.Resource.Expire(120) if err != nil { return "", nil, xerrors.Errorf("expire resource: %w", err) } - pool.MaxWait = 120 * time.Second + container.Pool.MaxWait = 120 * time.Second // Record the error that occurs during the retry. // The 'pool' pkg hardcodes a deadline error devoid // of any useful context. var retryErr error - err = pool.Retry(func() error { + err = container.Pool.Retry(func() error { db, err := sql.Open("postgres", dbURL) if err != nil { retryErr = xerrors.Errorf("open postgres: %w", err) @@ -155,8 +519,5 @@ func OpenContainerized(port int) (string, func(), error) { return "", nil, retryErr } - return dbURL, func() { - _ = pool.Purge(resource) - _ = os.RemoveAll(tempDir) - }, nil + return dbURL, containerCleanup, nil } diff --git a/coderd/database/dbtestutil/postgres_test.go b/coderd/database/dbtestutil/postgres_test.go index ec500d824a9ba..9cae9411289ad 100644 --- a/coderd/database/dbtestutil/postgres_test.go +++ b/coderd/database/dbtestutil/postgres_test.go @@ -11,25 +11,19 @@ import ( "go.uber.org/goleak" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/migrations" ) func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } -// nolint:paralleltest -func TestPostgres(t *testing.T) { - // postgres.Open() seems to be creating race conditions when run in parallel. - // t.Parallel() +func TestOpen(t *testing.T) { + t.Parallel() - if testing.Short() { - t.SkipNow() - return - } - - connect, closePg, err := dbtestutil.Open() + connect, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() + db, err := sql.Open("postgres", connect) require.NoError(t, err) err = db.Ping() @@ -37,3 +31,74 @@ func TestPostgres(t *testing.T) { err = db.Close() require.NoError(t, err) } + +func TestOpen_InvalidDBFrom(t *testing.T) { + t.Parallel() + + _, err := dbtestutil.Open(t, dbtestutil.WithDBFrom("__invalid__")) + require.Error(t, err) + require.ErrorContains(t, err, "template database") + require.ErrorContains(t, err, "does not exist") +} + +func TestOpen_ValidDBFrom(t *testing.T) { + t.Parallel() + + // first check if we can create a new template db + dsn, err := dbtestutil.Open(t, dbtestutil.WithDBFrom("")) + require.NoError(t, err) + + db, err := sql.Open("postgres", dsn) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + err = db.Ping() + require.NoError(t, err) + + templateDBName := "tpl_" + migrations.GetMigrationsHash()[:32] + tplDbExistsRes, err := db.Query("SELECT 1 FROM pg_database WHERE datname = $1", templateDBName) + if err != nil { + require.NoError(t, err) + } + require.True(t, tplDbExistsRes.Next()) + require.NoError(t, tplDbExistsRes.Close()) + + // now populate the db with some data and use it as a new template db + // to verify that dbtestutil.Open respects WithDBFrom + _, err = db.Exec("CREATE TABLE my_wonderful_table (id serial PRIMARY KEY, name text)") + require.NoError(t, err) + _, err = db.Exec("INSERT INTO my_wonderful_table (name) VALUES ('test')") + require.NoError(t, err) + + rows, err := db.Query("SELECT current_database()") + require.NoError(t, err) + require.True(t, rows.Next()) + var freshTemplateDBName string + require.NoError(t, rows.Scan(&freshTemplateDBName)) + require.NoError(t, rows.Close()) + require.NoError(t, db.Close()) + + for i := 0; i < 10; i++ { + db, err := sql.Open("postgres", dsn) + require.NoError(t, err) + require.NoError(t, db.Ping()) + require.NoError(t, db.Close()) + } + + // now create a new db from the template db + newDsn, err := dbtestutil.Open(t, dbtestutil.WithDBFrom(freshTemplateDBName)) + require.NoError(t, err) + + newDb, err := sql.Open("postgres", newDsn) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, newDb.Close()) + }) + + rows, err = newDb.Query("SELECT 1 FROM my_wonderful_table WHERE name = 'test'") + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Close()) +} diff --git a/coderd/database/gen/dump/main.go b/coderd/database/gen/dump/main.go index f563e1142619e..e3e80c528144e 100644 --- a/coderd/database/gen/dump/main.go +++ b/coderd/database/gen/dump/main.go @@ -2,6 +2,7 @@ package main import ( "database/sql" + "fmt" "os" "path/filepath" "runtime" @@ -12,12 +13,34 @@ import ( var preamble = []byte("-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.") +type mockTB struct { + cleanup []func() +} + +func (t *mockTB) Cleanup(f func()) { + t.cleanup = append(t.cleanup, f) +} + +func (*mockTB) Helper() { + // noop +} + +func (*mockTB) Logf(format string, args ...any) { + _, _ = fmt.Printf(format, args...) +} + func main() { - connection, closeFn, err := dbtestutil.Open() + t := &mockTB{} + defer func() { + for _, f := range t.cleanup { + f() + } + }() + + connection, err := dbtestutil.Open(t) if err != nil { panic(err) } - defer closeFn() db, err := sql.Open("postgres", connection) if err != nil { diff --git a/coderd/database/migrations/migrate.go b/coderd/database/migrations/migrate.go index 213408bbadd8c..c6c1b5740f873 100644 --- a/coderd/database/migrations/migrate.go +++ b/coderd/database/migrations/migrate.go @@ -2,11 +2,16 @@ package migrations import ( "context" + "crypto/sha256" "database/sql" "embed" "errors" + "fmt" "io/fs" "os" + "sort" + "strings" + "sync" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/source" @@ -17,6 +22,56 @@ import ( //go:embed *.sql var migrations embed.FS +var ( + migrationsHash string + migrationsHashOnce sync.Once +) + +// A migrations hash is a sha256 hash of the contents and names +// of the migrations sorted by filename. +func calculateMigrationsHash(migrationsFs embed.FS) (string, error) { + files, err := migrationsFs.ReadDir(".") + if err != nil { + return "", xerrors.Errorf("read migrations directory: %w", err) + } + sortedFiles := make([]fs.DirEntry, len(files)) + copy(sortedFiles, files) + sort.Slice(sortedFiles, func(i, j int) bool { + return sortedFiles[i].Name() < sortedFiles[j].Name() + }) + + var builder strings.Builder + for _, file := range sortedFiles { + if _, err := builder.WriteString(file.Name()); err != nil { + return "", xerrors.Errorf("write migration file name %q: %w", file.Name(), err) + } + content, err := migrationsFs.ReadFile(file.Name()) + if err != nil { + return "", xerrors.Errorf("read migration file %q: %w", file.Name(), err) + } + if _, err := builder.Write(content); err != nil { + return "", xerrors.Errorf("write migration file content %q: %w", file.Name(), err) + } + } + + hash := sha256.New() + if _, err := hash.Write([]byte(builder.String())); err != nil { + return "", xerrors.Errorf("write to hash: %w", err) + } + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} + +func GetMigrationsHash() string { + migrationsHashOnce.Do(func() { + hash, err := calculateMigrationsHash(migrations) + if err != nil { + panic(err) + } + migrationsHash = hash + }) + return migrationsHash +} + func setup(db *sql.DB, migs fs.FS) (source.Driver, *migrate.Migrate, error) { if migs == nil { migs = migrations diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index 51e7fcc86cb03..c64c2436da18d 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -95,9 +95,8 @@ func TestMigrate(t *testing.T) { func testSQLDB(t testing.TB) *sql.DB { t.Helper() - connection, closeFn, err := dbtestutil.Open() + connection, err := dbtestutil.Open(t) require.NoError(t, err) - t.Cleanup(closeFn) db, err := sql.Open("postgres", connection) require.NoError(t, err) diff --git a/coderd/database/pubsub/pubsub_linux_test.go b/coderd/database/pubsub/pubsub_linux_test.go index f208af921b441..819de0a71ba52 100644 --- a/coderd/database/pubsub/pubsub_linux_test.go +++ b/coderd/database/pubsub/pubsub_linux_test.go @@ -40,9 +40,8 @@ func TestPubsub(t *testing.T) { defer cancelFunc() logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) defer db.Close() @@ -69,9 +68,8 @@ func TestPubsub(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) defer db.Close() @@ -85,9 +83,8 @@ func TestPubsub(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) defer db.Close() @@ -122,9 +119,8 @@ func TestPubsub_ordering(t *testing.T) { defer cancelFunc() logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) defer db.Close() @@ -167,7 +163,7 @@ const disconnectTestPort = 26892 func TestPubsub_Disconnect(t *testing.T) { // we always use a Docker container for this test, even in CI, since we need to be able to kill // postgres and bring it back on the same port. - connectionURL, closePg, err := dbtestutil.OpenContainerized(disconnectTestPort) + connectionURL, closePg, err := dbtestutil.OpenContainerized(t, dbtestutil.DBContainerOptions{Port: disconnectTestPort}) require.NoError(t, err) defer closePg() db, err := sql.Open("postgres", connectionURL) @@ -238,7 +234,7 @@ func TestPubsub_Disconnect(t *testing.T) { // restart postgres on the same port --- since we only use LISTEN/NOTIFY it doesn't // matter that the new postgres doesn't have any persisted state from before. - _, closeNewPg, err := dbtestutil.OpenContainerized(disconnectTestPort) + _, closeNewPg, err := dbtestutil.OpenContainerized(t, dbtestutil.DBContainerOptions{Port: disconnectTestPort}) require.NoError(t, err) defer closeNewPg() @@ -305,7 +301,7 @@ func TestMeasureLatency(t *testing.T) { newPubsub := func() (pubsub.Pubsub, func()) { ctx, cancel := context.WithCancel(context.Background()) logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) @@ -315,7 +311,6 @@ func TestMeasureLatency(t *testing.T) { return ps, func() { _ = ps.Close() _ = db.Close() - closePg() cancel() } } diff --git a/coderd/database/pubsub/pubsub_test.go b/coderd/database/pubsub/pubsub_test.go index 21b4b1d54c171..6b8181ea7d834 100644 --- a/coderd/database/pubsub/pubsub_test.go +++ b/coderd/database/pubsub/pubsub_test.go @@ -24,9 +24,8 @@ func TestPGPubsub_Metrics(t *testing.T) { } logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() db, err := sql.Open("postgres", connectionURL) require.NoError(t, err) defer db.Close() @@ -132,9 +131,8 @@ func TestPGPubsubDriver(t *testing.T) { IgnoreErrors: true, }).Leveled(slog.LevelDebug) - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - defer closePg() // use a separate subber and pubber so we can keep track of listener connections db, err := sql.Open("postgres", connectionURL) diff --git a/enterprise/cli/server_dbcrypt_test.go b/enterprise/cli/server_dbcrypt_test.go index b1767889d9c33..070f172bcbe7b 100644 --- a/enterprise/cli/server_dbcrypt_test.go +++ b/enterprise/cli/server_dbcrypt_test.go @@ -32,9 +32,8 @@ func TestServerDBCrypt(t *testing.T) { t.Cleanup(cancel) // Setup a postgres database. - connectionURL, closePg, err := dbtestutil.Open() + connectionURL, err := dbtestutil.Open(t) require.NoError(t, err) - t.Cleanup(closePg) t.Cleanup(func() { dbtestutil.DumpOnFailure(t, connectionURL) }) sqlDB, err := sql.Open("postgres", connectionURL) diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go index c0d122aa74992..49248e636f04b 100644 --- a/enterprise/tailnet/pgcoord_test.go +++ b/enterprise/tailnet/pgcoord_test.go @@ -798,9 +798,8 @@ func TestPGCoordinatorDual_FailedHeartbeat(t *testing.T) { t.Skip("test only with postgres") } - dburl, closeFn, err := dbtestutil.Open() + dburl, err := dbtestutil.Open(t) require.NoError(t, err) - t.Cleanup(closeFn) store1, ps1, sdb1 := dbtestutil.NewDBWithSQLDB(t, dbtestutil.WithURL(dburl)) defer sdb1.Close() From 735e965bdd2e46151f8e66a98a0e34226a0f601d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:35:02 -0300 Subject: [PATCH 053/223] chore: bump @types/lodash from 4.17.9 to 4.17.13 in /site (#15334) --- site/package.json | 2 +- site/pnpm-lock.yaml | 12 ++++++------ site/src/pages/AuditPage/AuditFilter.tsx | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/site/package.json b/site/package.json index 3341749c4bcb7..a85993e449084 100644 --- a/site/package.json +++ b/site/package.json @@ -127,7 +127,7 @@ "@types/express": "4.17.17", "@types/file-saver": "2.0.7", "@types/jest": "29.5.14", - "@types/lodash": "4.17.9", + "@types/lodash": "4.17.13", "@types/node": "20.16.10", "@types/react": "18.3.12", "@types/react-color": "3.0.12", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 373a642762fad..329f5f6bddedc 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -290,8 +290,8 @@ importers: specifier: 29.5.14 version: 29.5.14 '@types/lodash': - specifier: 4.17.9 - version: 4.17.9 + specifier: 4.17.13 + version: 4.17.13 '@types/node': specifier: 20.16.10 version: 20.16.10 @@ -2508,8 +2508,8 @@ packages: '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} - '@types/lodash@4.17.9': - resolution: {integrity: sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==} + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} '@types/mdast@4.0.3': resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} @@ -7773,7 +7773,7 @@ snapshots: '@storybook/preview-api': 8.1.11 '@storybook/theming': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/types': 8.1.11 - '@types/lodash': 4.17.9 + '@types/lodash': 4.17.13 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 @@ -8478,7 +8478,7 @@ snapshots: '@types/tough-cookie': 4.0.2 parse5: 7.1.2 - '@types/lodash@4.17.9': {} + '@types/lodash@4.17.13': {} '@types/mdast@4.0.3': dependencies: diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 05f48d7c2103e..42a096dd8144a 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -121,7 +121,7 @@ export const useResourceTypeFilterMenu = ({ onChange, }: Pick) => { const actionOptions: SelectFilterOption[] = ResourceTypes.map((type) => { - let label = capitalize(type); + let label: string = capitalize(type); if (type === "api_key") { label = "API Key"; From f25a07502aafefbcfb41e8c7efa32fa2f5cf1024 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 4 Nov 2024 09:55:40 -0800 Subject: [PATCH 054/223] chore: fix links in README.md (#15366) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 395534c44dd9c..2048f6ba1fd83 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@

-[Quickstart](#quickstart) | [Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Enterprise](https://coder.com/docs/enterprise) +[Quickstart](#quickstart) | [Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Premium](https://coder.com/pricing#compare-plans) [![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder) [![release](https://img.shields.io/github/v/release/coder/coder)](https://github.com/coder/coder/releases/latest) [![godoc](https://pkg.go.dev/badge/github.com/coder/coder.svg)](https://pkg.go.dev/github.com/coder/coder) [![Go Report Card](https://goreportcard.com/badge/github.com/coder/coder/v2)](https://goreportcard.com/report/github.com/coder/coder/v2) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9511/badge)](https://www.bestpractices.dev/projects/9511) -[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/coder/coder/badge)](https://api.securityscorecards.dev/projects/github.com/coder/coder) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/coder/coder/badge)](https://scorecard.dev/viewer/?uri=github.com%2Fcoder%2Fcoder) [![license](https://img.shields.io/github/license/coder/coder)](./LICENSE) From cfc4cb07b653a5b646c5f869ace2195f96c20c7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:56:00 -0700 Subject: [PATCH 055/223] chore: bump @types/node from 20.16.10 to 20.17.6 in /site (#15351) --- site/package.json | 2 +- site/pnpm-lock.yaml | 168 ++++++++++++++++++++++---------------------- 2 files changed, 85 insertions(+), 85 deletions(-) diff --git a/site/package.json b/site/package.json index a85993e449084..710bd7e49787b 100644 --- a/site/package.json +++ b/site/package.json @@ -128,7 +128,7 @@ "@types/file-saver": "2.0.7", "@types/jest": "29.5.14", "@types/lodash": "4.17.13", - "@types/node": "20.16.10", + "@types/node": "20.17.6", "@types/react": "18.3.12", "@types/react-color": "3.0.12", "@types/react-date-range": "1.4.4", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 329f5f6bddedc..e7ddc66058018 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -234,7 +234,7 @@ importers: version: 8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-interactions': specifier: 8.1.11 - version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) + version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))) '@storybook/addon-links': specifier: 8.1.11 version: 8.1.11(react@18.3.1) @@ -252,10 +252,10 @@ importers: version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@storybook/react-vite': specifier: 8.1.11 - version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) + version: 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)) '@storybook/test': specifier: 8.1.11 - version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) + version: 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))) '@swc/core': specifier: 1.3.38 version: 1.3.38 @@ -264,7 +264,7 @@ importers: version: 0.2.37(@swc/core@1.3.38) '@testing-library/jest-dom': specifier: 6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) + version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))) '@testing-library/react': specifier: 14.3.1 version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -293,8 +293,8 @@ importers: specifier: 4.17.13 version: 4.17.13 '@types/node': - specifier: 20.16.10 - version: 20.16.10 + specifier: 20.17.6 + version: 20.17.6 '@types/react': specifier: 18.3.12 version: 18.3.12 @@ -330,7 +330,7 @@ importers: version: 9.0.2 '@vitejs/plugin-react': specifier: 4.3.3 - version: 4.3.3(vite@5.4.10(@types/node@20.16.10)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) chromatic: specifier: 11.16.3 version: 11.16.3 @@ -342,7 +342,7 @@ importers: version: 4.21.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + version: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) jest-canvas-mock: specifier: 2.5.2 version: 2.5.2 @@ -384,7 +384,7 @@ importers: version: 0.6.0(react-dom@18.3.1(react@18.3.1)) ts-node: specifier: 10.9.1 - version: 10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3) + version: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3) ts-proto: specifier: 1.164.0 version: 1.164.0 @@ -396,10 +396,10 @@ importers: version: 5.6.3 vite: specifier: 5.4.10 - version: 5.4.10(@types/node@20.16.10) + version: 5.4.10(@types/node@20.17.6) vite-plugin-checker: specifier: 0.8.0 - version: 0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) + version: 0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)) vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 @@ -2535,8 +2535,8 @@ packages: '@types/node@18.19.0': resolution: {integrity: sha512-667KNhaD7U29mT5wf+TZUnrzPrlL2GNQ5N0BMjO2oNULhBxX0/FKCkm6JMu0Jh7Z+1LwUlR21ekd7KhIboNFNw==} - '@types/node@20.16.10': - resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==} + '@types/node@20.17.6': + resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} '@types/parse-json@4.0.0': resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} @@ -6858,7 +6858,7 @@ snapshots: dependencies: '@inquirer/type': 1.2.0 '@types/mute-stream': 0.0.4 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -6897,27 +6897,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6946,14 +6946,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.6.2 '@jest/types': 29.6.1 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-mock: 29.6.2 '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -6971,7 +6971,7 @@ snapshots: dependencies: '@jest/types': 29.6.1 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-message-util: 29.6.2 jest-mock: 29.6.2 jest-util: 29.6.2 @@ -6980,7 +6980,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -7002,7 +7002,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -7072,7 +7072,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.5 '@types/istanbul-reports': 3.0.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/yargs': 17.0.29 chalk: 4.1.2 @@ -7081,17 +7081,17 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.6.3) - vite: 5.4.10(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.17.6) optionalDependencies: typescript: 5.6.3 @@ -7670,11 +7670,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': + '@storybook/addon-interactions@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.11 - '@storybook/test': 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) + '@storybook/test': 8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))) '@storybook/types': 8.1.11 polished: 4.2.2 ts-dedent: 2.2.0 @@ -7795,7 +7795,7 @@ snapshots: - prettier - supports-color - '@storybook/builder-vite@8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10))': + '@storybook/builder-vite@8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6))': dependencies: '@storybook/channels': 8.1.11 '@storybook/client-logger': 8.1.11 @@ -7814,7 +7814,7 @@ snapshots: fs-extra: 11.2.0 magic-string: 0.30.5 ts-dedent: 2.2.0 - vite: 5.4.10(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.17.6) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: @@ -8041,11 +8041,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-vite@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10))': + '@storybook/react-vite@8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)) '@rollup/pluginutils': 5.0.5(rollup@4.24.3) - '@storybook/builder-vite': 8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)) + '@storybook/builder-vite': 8.1.11(prettier@3.3.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)) '@storybook/node-logger': 8.1.11 '@storybook/react': 8.1.11(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@storybook/types': 8.1.11 @@ -8056,7 +8056,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 tsconfig-paths: 4.2.0 - vite: 5.4.10(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.17.6) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -8119,14 +8119,14 @@ snapshots: core-js: 3.32.0 find-up: 4.1.0 - '@storybook/test@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': + '@storybook/test@8.1.11(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)))': dependencies: '@storybook/client-logger': 8.1.11 '@storybook/core-events': 8.1.11 '@storybook/instrumenter': 8.1.11 '@storybook/preview-api': 8.1.11 '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3))) + '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 @@ -8260,7 +8260,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': + '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.25.6 @@ -8273,9 +8273,9 @@ snapshots: optionalDependencies: '@jest/globals': 29.7.0 '@types/jest': 29.5.14 - jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + jest: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)))': + '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.7 @@ -8288,7 +8288,7 @@ snapshots: optionalDependencies: '@jest/globals': 29.7.0 '@types/jest': 29.5.14 - jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + jest: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) '@testing-library/react-hooks@8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -8362,7 +8362,7 @@ snapshots: '@types/body-parser@1.19.2': dependencies: '@types/connect': 3.4.35 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/chroma-js@2.4.0': {} @@ -8374,7 +8374,7 @@ snapshots: '@types/connect@3.4.35': dependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/cookie@0.6.0': {} @@ -8398,7 +8398,7 @@ snapshots: '@types/express-serve-static-core@4.17.35': dependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -8424,11 +8424,11 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/hast@2.3.8': dependencies: @@ -8474,7 +8474,7 @@ snapshots: '@types/jsdom@20.0.1': dependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 @@ -8496,13 +8496,13 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/node@18.19.0': dependencies: undici-types: 5.26.5 - '@types/node@20.16.10': + '@types/node@20.17.6': dependencies: undici-types: 6.19.8 @@ -8564,13 +8564,13 @@ snapshots: '@types/send@0.17.1': dependencies: '@types/mime': 1.3.2 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/serve-static@1.15.2': dependencies: '@types/http-errors': 2.0.1 '@types/mime': 3.0.1 - '@types/node': 20.16.10 + '@types/node': 20.17.6 '@types/ssh2@1.15.1': dependencies: @@ -8612,14 +8612,14 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react@4.3.3(vite@5.4.10(@types/node@20.16.10))': + '@vitejs/plugin-react@4.3.3(vite@5.4.10(@types/node@20.17.6))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.10(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.17.6) transitivePeerDependencies: - supports-color @@ -9151,13 +9151,13 @@ snapshots: nan: 2.20.0 optional: true - create-jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): + create-jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -10264,7 +10264,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3(babel-plugin-macros@3.1.0) @@ -10284,16 +10284,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): + jest-cli@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + create-jest: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -10303,7 +10303,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): + jest-config@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -10328,8 +10328,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.16.10 - ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3) + '@types/node': 20.17.6 + ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10366,7 +10366,7 @@ snapshots: '@jest/fake-timers': 29.6.2 '@jest/types': 29.6.1 '@types/jsdom': 20.0.1 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-mock: 29.6.2 jest-util: 29.6.2 jsdom: 20.0.3(canvas@3.0.0-rc2) @@ -10382,7 +10382,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -10394,7 +10394,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.16.10 + '@types/node': 20.17.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -10450,13 +10450,13 @@ snapshots: jest-mock@29.6.2: dependencies: '@jest/types': 29.6.1 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-util: 29.6.2 jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -10491,7 +10491,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -10519,7 +10519,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 @@ -10565,7 +10565,7 @@ snapshots: jest-util@29.6.2: dependencies: '@jest/types': 29.6.1 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10574,7 +10574,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10593,7 +10593,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.16.10 + '@types/node': 20.17.6 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -10607,17 +10607,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)): + jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.16.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3)) + jest-cli: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -11466,7 +11466,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.16.10 + '@types/node': 20.17.6 long: 5.2.3 proxy-addr@2.0.7: @@ -12266,14 +12266,14 @@ snapshots: '@ts-morph/common': 0.12.3 code-block-writer: 11.0.3 - ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.16.10)(typescript@5.6.3): + ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.16.10 + '@types/node': 20.17.6 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -12491,7 +12491,7 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-plugin-checker@0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.16.10)): + vite-plugin-checker@0.8.0(@biomejs/biome@1.9.3)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)): dependencies: '@babel/code-frame': 7.25.7 ansi-escapes: 4.3.2 @@ -12503,7 +12503,7 @@ snapshots: npm-run-path: 4.0.1 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - vite: 5.4.10(@types/node@20.16.10) + vite: 5.4.10(@types/node@20.17.6) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.12 @@ -12516,13 +12516,13 @@ snapshots: vite-plugin-turbosnap@1.0.3: {} - vite@5.4.10(@types/node@20.16.10): + vite@5.4.10(@types/node@20.17.6): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.3 optionalDependencies: - '@types/node': 20.16.10 + '@types/node': 20.17.6 fsevents: 2.3.3 vscode-jsonrpc@6.0.0: {} From dc29b812861b3db92026e0779d74eea7d8018e2d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 4 Nov 2024 18:17:16 +0000 Subject: [PATCH 056/223] fix(site/static/icon): add filebrowser icon (#15367) Fixes https://github.com/coder/coder/issues/15365 We used to hit https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg for the filebrowser icon but coder/modules#334 modified the icon URL to point to a self-hosted icon. I simply copied the icon from the `coder/modules` repo. --- site/src/theme/icons.json | 1 + site/static/icon/filebrowser.svg | 147 +++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 site/static/icon/filebrowser.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index 7fea2d79bd29e..b99585ab34b3f 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -35,6 +35,7 @@ "dotfiles.svg", "dotnet.svg", "fedora.svg", + "filebrowser.svg", "fleet.svg", "fly.io.svg", "folder.svg", diff --git a/site/static/icon/filebrowser.svg b/site/static/icon/filebrowser.svg new file mode 100644 index 0000000000000..5e78eccff1adb --- /dev/null +++ b/site/static/icon/filebrowser.svg @@ -0,0 +1,147 @@ + +image/svg+xml + + + + + \ No newline at end of file From 2cf745766ca234b4287f93ef3ed425dd193b85b2 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 4 Nov 2024 10:26:04 -0800 Subject: [PATCH 057/223] chore: use typos extension in vscode (#15136) This synchronises the spellchecker with our CI. We use https://github.com/crate-ci/typos in CI, so let us use typos in vscode too. https://marketplace.visualstudio.com/items?itemName=tekumara.typos-vscode --- .vscode/extensions.json | 2 +- .vscode/settings.json | 205 +--------------------------------------- 2 files changed, 3 insertions(+), 204 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c885d6edf354f..713512c5e1030 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,7 +8,7 @@ "emeraldwalk.runonsave", "zxh404.vscode-proto3", "redhat.vscode-yaml", - "streetsidesoftware.code-spell-checker", + "tekumara.typos-vscode", "EditorConfig.EditorConfig", "biomejs.biome" ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 6695a12faa8dc..e78b37319b32b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,206 +1,4 @@ { - "cSpell.words": [ - "afero", - "agentsdk", - "apps", - "ASKPASS", - "authcheck", - "autostop", - "autoupdate", - "awsidentity", - "bodyclose", - "buildinfo", - "buildname", - "Caddyfile", - "circbuf", - "cliflag", - "cliui", - "codecov", - "codercom", - "coderd", - "coderdenttest", - "coderdtest", - "codersdk", - "contravariance", - "cronstrue", - "databasefake", - "dbcrypt", - "dbgen", - "dbmem", - "dbtype", - "DERP", - "derphttp", - "derpmap", - "devcontainers", - "devel", - "devtunnel", - "dflags", - "dogfood", - "dotfiles", - "drpc", - "drpcconn", - "drpcmux", - "drpcserver", - "Dsts", - "embeddedpostgres", - "enablements", - "enterprisemeta", - "Entra", - "errgroup", - "eventsourcemock", - "externalauth", - "Failf", - "fatih", - "filebrowser", - "Formik", - "gitauth", - "Gitea", - "gitsshkey", - "goarch", - "gographviz", - "goleak", - "gonet", - "googleclouddns", - "gossh", - "gsyslog", - "GTTY", - "hashicorp", - "hclsyntax", - "httpapi", - "httpmw", - "idtoken", - "Iflag", - "incpatch", - "initialisms", - "ipnstate", - "isatty", - "jetbrains", - "Jobf", - "Keygen", - "kirsle", - "knowledgebase", - "Kubernetes", - "ldflags", - "magicsock", - "manifoldco", - "mapstructure", - "mattn", - "mitchellh", - "moby", - "namesgenerator", - "namespacing", - "netaddr", - "netcheck", - "netip", - "netmap", - "netns", - "netstack", - "nettype", - "nfpms", - "nhooyr", - "nmcfg", - "nolint", - "nosec", - "ntqry", - "OIDC", - "oneof", - "opty", - "paralleltest", - "parameterscopeid", - "portsharing", - "pqtype", - "prometheusmetrics", - "promhttp", - "protobuf", - "provisionerd", - "provisionerdserver", - "provisionersdk", - "psql", - "ptrace", - "ptty", - "ptys", - "ptytest", - "quickstart", - "reconfig", - "replicasync", - "retrier", - "rpty", - "SCIM", - "sdkproto", - "sdktrace", - "Signup", - "slogtest", - "sourcemapped", - "speedtest", - "spinbutton", - "Srcs", - "stdbuf", - "stretchr", - "STTY", - "stuntest", - "subpage", - "tailbroker", - "tailcfg", - "tailexchange", - "tailnet", - "tailnettest", - "Tailscale", - "tanstack", - "tbody", - "TCGETS", - "tcpip", - "TCSETS", - "templateversions", - "testdata", - "testid", - "testutil", - "tfexec", - "tfjson", - "tfplan", - "tfstate", - "thead", - "tios", - "tmpdir", - "tokenconfig", - "Topbar", - "tparallel", - "trialer", - "trimprefix", - "tsdial", - "tslogger", - "tstun", - "turnconn", - "typegen", - "typesafe", - "unauthenticate", - "unconvert", - "untar", - "userauth", - "userspace", - "VMID", - "walkthrough", - "weblinks", - "webrtc", - "websockets", - "wgcfg", - "wgconfig", - "wgengine", - "wgmonitor", - "wgnet", - "workspaceagent", - "workspaceagents", - "workspaceapp", - "workspaceapps", - "workspacebuilds", - "workspacename", - "workspaceproxies", - "wsjson", - "xerrors", - "xlarge", - "xsmall", - "yamux" - ], - "cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"], "emeraldwalk.runonsave": { "commands": [ { @@ -257,5 +55,6 @@ "[css][html][markdown][yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "typos.config": ".github/workflows/typos.toml" } From 8024c1dff45904bec41b130e963a58c1282b5835 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 4 Nov 2024 20:45:43 -0600 Subject: [PATCH 058/223] fix: allow workspace owners to view timings (#15364) Anyone with authz access to a workspace should be able to read timings information of its builds. To do this without `AsSystemContext` would do an extra 4 db calls. --- coderd/workspacebuilds.go | 6 ++++-- coderd/workspacebuilds_test.go | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 44c34d8a25da3..fa88a72cf0702 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -952,7 +952,8 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching provisioner job timings: %w", err) } - agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID) + //nolint:gocritic // Already checked if the build can be fetched. + agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(dbauthz.AsSystemRestricted(ctx), build.ID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agent script timings: %w", err) } @@ -965,7 +966,8 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) for _, resource := range resources { resourceIDs = append(resourceIDs, resource.ID) } - agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) + //nolint:gocritic // Already checked if the build can be fetched. + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(dbauthz.AsSystemRestricted(ctx), resourceIDs) if err != nil && !errors.Is(err, sql.ErrNoRows) { return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agents: %w", err) } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 3aae3989df5b4..add68ed7dcfd6 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1188,17 +1188,19 @@ func TestWorkspaceBuildTimings(t *testing.T) { // Setup the test environment with a template and version db, pubsub := dbtestutil.NewDB(t) - client := coderdtest.New(t, &coderdtest.Options{ + ownerClient := coderdtest.New(t, &coderdtest.Options{ Database: db, Pubsub: pubsub, }) - owner := coderdtest.CreateFirstUser(t, client) + owner := coderdtest.CreateFirstUser(t, ownerClient) + client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) + file := dbgen.File(t, db, database.File{ CreatedBy: owner.UserID, }) versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{ OrganizationID: owner.OrganizationID, - InitiatorID: owner.UserID, + InitiatorID: user.ID, FileID: file.ID, Tags: database.StringMap{ "custom": "true", @@ -1219,7 +1221,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { // build number, each test will have its own workspace and build. makeBuild := func() database.WorkspaceBuild { ws := dbgen.Workspace(t, db, database.WorkspaceTable{ - OwnerID: owner.UserID, + OwnerID: user.ID, OrganizationID: owner.OrganizationID, TemplateID: template.ID, }) From 98e584b36f3a1045a177a2e09b1273ddb168b865 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 08:59:20 +0500 Subject: [PATCH 059/223] chore: bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#15371) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 63ffddab6db7f..49603fe69e8a3 100644 --- a/go.mod +++ b/go.mod @@ -115,7 +115,7 @@ require ( github.com/go-playground/validator/v10 v10.22.0 github.com/gofrs/flock v0.12.0 github.com/gohugoio/hugo v0.136.5 - github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang-jwt/jwt/v4 v4.5.1 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 diff --git a/go.sum b/go.sum index fc08a7d7a0fd6..5f7ad74b2e704 100644 --- a/go.sum +++ b/go.sum @@ -452,8 +452,8 @@ github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XG github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= From 076399b3bd2fc15cf2b98a816f0d665b2fe2fc43 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 4 Nov 2024 20:41:48 -0800 Subject: [PATCH 060/223] chore: correct typos and configure permissions in pr-deploy.yaml (#15372) --- .github/workflows/pr-deploy.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index fc59ae85289e3..2ef388f7f9221 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -162,7 +162,7 @@ jobs: set -euo pipefail # build if the workflow is manually triggered and the deployment doesn't exist (first build or force rebuild) echo "first_or_force_build=${{ (github.event_name == 'workflow_dispatch' && steps.check_deployment.outputs.NEW == 'true') || github.event.inputs.build == 'true' }}" >> $GITHUB_OUTPUT - # build if the deployment alreday exist and there are changes in the files that we care about (automatic updates) + # build if the deployment already exist and there are changes in the files that we care about (automatic updates) echo "automatic_rebuild=${{ steps.check_deployment.outputs.NEW == 'false' && steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }}" >> $GITHUB_OUTPUT comment-pr: @@ -208,7 +208,7 @@ jobs: permissions: # Necessary to push docker images to ghcr.io. packages: write - # This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs chnages. + # This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs changes. concurrency: group: build-${{ github.workflow }}-${{ github.ref }}-${{ needs.get_info.outputs.BUILD }} cancel-in-progress: true @@ -265,6 +265,8 @@ jobs: always() && (needs.build.result == 'success' || needs.build.result == 'skipped') && (needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true') runs-on: "ubuntu-latest" + permissions: + pull-requests: write # needed for commenting on PRs env: CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }} From 871cc05e995c8975ae2eeed45276233dc1604101 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:23:16 +1100 Subject: [PATCH 061/223] chore: add a dns.OSConfigurator implementation that uses the CoderVPN protocol (#15342) Closes #14733. --- tailnet/conn.go | 5 +++ vpn/dns.go | 58 +++++++++++++++++++++++++++++++ vpn/dns_internal_test.go | 73 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 vpn/dns.go create mode 100644 vpn/dns_internal_test.go diff --git a/tailnet/conn.go b/tailnet/conn.go index 1217bdeb6f0f7..be098d16085c4 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -22,6 +22,7 @@ import ( "tailscale.com/envknob" "tailscale.com/ipn/ipnstate" "tailscale.com/net/connstats" + "tailscale.com/net/dns" "tailscale.com/net/netmon" "tailscale.com/net/netns" "tailscale.com/net/tsdial" @@ -106,6 +107,9 @@ type Options struct { ClientType proto.TelemetryEvent_ClientType // TelemetrySink is optional. TelemetrySink TelemetrySink + // DNSConfigurator is optional, and is passed to the underlying wireguard + // engine. + DNSConfigurator dns.OSConfigurator } // TelemetrySink allows tailnet.Conn to send network telemetry to the Coder @@ -178,6 +182,7 @@ func NewConn(options *Options) (conn *Conn, err error) { Dialer: dialer, ListenPort: options.ListenPort, SetSubsystem: sys.Set, + DNS: options.DNSConfigurator, }) if err != nil { return nil, xerrors.Errorf("create wgengine: %w", err) diff --git a/vpn/dns.go b/vpn/dns.go new file mode 100644 index 0000000000000..7e4ea5bbd29a0 --- /dev/null +++ b/vpn/dns.go @@ -0,0 +1,58 @@ +package vpn + +import "tailscale.com/net/dns" + +func NewDNSConfigurator(t *Tunnel) dns.OSConfigurator { + return &dnsManager{tunnel: t} +} + +type dnsManager struct { + tunnel *Tunnel +} + +func (v *dnsManager) SetDNS(cfg dns.OSConfig) error { + settings := convertDNSConfig(cfg) + return v.tunnel.ApplyNetworkSettings(v.tunnel.ctx, &NetworkSettingsRequest{ + DnsSettings: settings, + }) +} + +func (*dnsManager) GetBaseConfig() (dns.OSConfig, error) { + // Tailscale calls this function to blend the OS's DNS configuration with + // it's own, so this is only called if `SupportsSplitDNS` returns false. + return dns.OSConfig{}, dns.ErrGetBaseConfigNotSupported +} + +func (*dnsManager) SupportsSplitDNS() bool { + // macOS & Windows 10+ support split DNS, so we'll assume all CoderVPN + // clients do too. + return true +} + +// Close implements dns.OSConfigurator. +func (*dnsManager) Close() error { + // There's no cleanup that we need to initiate from within the dylib. + return nil +} + +func convertDNSConfig(cfg dns.OSConfig) *NetworkSettingsRequest_DNSSettings { + servers := make([]string, 0, len(cfg.Nameservers)) + for _, ns := range cfg.Nameservers { + servers = append(servers, ns.String()) + } + searchDomains := make([]string, 0, len(cfg.SearchDomains)) + for _, domain := range cfg.SearchDomains { + searchDomains = append(searchDomains, domain.WithoutTrailingDot()) + } + matchDomains := make([]string, 0, len(cfg.MatchDomains)) + for _, domain := range cfg.MatchDomains { + matchDomains = append(matchDomains, domain.WithoutTrailingDot()) + } + return &NetworkSettingsRequest_DNSSettings{ + Servers: servers, + SearchDomains: searchDomains, + DomainName: "coder", + MatchDomains: matchDomains, + MatchDomainsNoSearch: false, + } +} diff --git a/vpn/dns_internal_test.go b/vpn/dns_internal_test.go new file mode 100644 index 0000000000000..a4fa61aec1d66 --- /dev/null +++ b/vpn/dns_internal_test.go @@ -0,0 +1,73 @@ +package vpn + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/require" + "tailscale.com/net/dns" + "tailscale.com/util/dnsname" +) + +func TestConvertDNSConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input dns.OSConfig + expected *NetworkSettingsRequest_DNSSettings + }{ + { + name: "Basic", + input: dns.OSConfig{ + Nameservers: []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("8.8.8.8"), + }, + SearchDomains: []dnsname.FQDN{ + "example.com.", + "test.local.", + }, + MatchDomains: []dnsname.FQDN{ + "internal.domain.", + }, + }, + expected: &NetworkSettingsRequest_DNSSettings{ + Servers: []string{"1.1.1.1", "8.8.8.8"}, + SearchDomains: []string{"example.com", "test.local"}, + DomainName: "coder", + MatchDomains: []string{"internal.domain"}, + MatchDomainsNoSearch: false, + }, + }, + { + name: "Empty", + input: dns.OSConfig{ + Nameservers: []netip.Addr{}, + SearchDomains: []dnsname.FQDN{}, + MatchDomains: []dnsname.FQDN{}, + }, + expected: &NetworkSettingsRequest_DNSSettings{ + Servers: []string{}, + SearchDomains: []string{}, + DomainName: "coder", + MatchDomains: []string{}, + MatchDomainsNoSearch: false, + }, + }, + } + + //nolint:paralleltest // outdated rule + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := convertDNSConfig(tt.input) + require.Equal(t, tt.expected.Servers, result.Servers) + require.Equal(t, tt.expected.SearchDomains, result.SearchDomains) + require.Equal(t, tt.expected.DomainName, result.DomainName) + require.Equal(t, tt.expected.MatchDomains, result.MatchDomains) + require.Equal(t, tt.expected.MatchDomainsNoSearch, result.MatchDomainsNoSearch) + }) + } +} From 765314ce181fe78c6164abb45426754ec8ebe85f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:43:41 +1100 Subject: [PATCH 062/223] ci: bump the github-actions group with 4 updates (#15359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 4 updates: [crate-ci/typos](https://github.com/crate-ci/typos), [google-github-actions/auth](https://github.com/google-github-actions/auth), [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud) and [google-github-actions/get-gke-credentials](https://github.com/google-github-actions/get-gke-credentials). Updates `crate-ci/typos` from 1.26.8 to 1.27.0
Release notes

Sourced from crate-ci/typos's releases.

v1.27.0

[1.27.0] - 2024-11-01

Features

Changelog

Sourced from crate-ci/typos's changelog.

Change Log

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased] - ReleaseDate

[1.27.0] - 2024-11-01

Features

[1.26.8] - 2024-10-24

[1.26.7] - 2024-10-24

[1.26.6] - 2024-10-24

[1.26.5] - 2024-10-24

[1.26.4] - 2024-10-24

[1.26.3] - 2024-10-24

Fixes

  • Accept additionals

[1.26.2] - 2024-10-24

Fixes

  • Accept tesselate variants

[1.26.1] - 2024-10-23

Fixes

  • Respect --force-exclude for binary files

[1.26.0] - 2024-10-07

Compatibility

  • (pre-commit) Requires 3.2+

Fixes

... (truncated)

Commits

Updates `google-github-actions/auth` from 2.1.6 to 2.1.7
Release notes

Sourced from google-github-actions/auth's releases.

v2.1.7

What's Changed

Full Changelog: https://github.com/google-github-actions/auth/compare/v2.1.6...v2.1.7

Commits

Updates `google-github-actions/setup-gcloud` from 2.1.1 to 2.1.2
Release notes

Sourced from google-github-actions/setup-gcloud's releases.

v2.1.2

What's Changed

Full Changelog: https://github.com/google-github-actions/setup-gcloud/compare/v2.1.1...v2.1.2

Commits

Updates `google-github-actions/get-gke-credentials` from 2.2.1 to 2.2.2
Release notes

Sourced from google-github-actions/get-gke-credentials's releases.

v2.2.2

What's Changed

Full Changelog: https://github.com/google-github-actions/get-gke-credentials/compare/v2.2.1...v2.2.2

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Muhammad Atif Ali --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/dogfood.yaml | 2 +- .github/workflows/release.yaml | 8 ++++---- .github/workflows/typos.toml | 1 + coderd/database/dbmem/dbmem.go | 12 ++++++------ coderd/httpmw/workspaceproxy.go | 2 +- coderd/unhanger/detector.go | 14 +++++++------- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7773ac759e0d9..55e14f0e6e1f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -188,7 +188,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@0d9e0c2c1bd7f770f6eb90f87780848ca02fc12c # v1.26.8 + uses: crate-ci/typos@d01f29c66d1bf1a08730750f61d86c210b0d039d # v1.27.0 with: config: .github/workflows/typos.toml @@ -954,13 +954,13 @@ jobs: fetch-depth: 0 - name: Authenticate to Google Cloud - uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@f0990588f1e5b5af6827153b93673613abdc6ec7 # v2.1.1 + uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # v2.1.2 - name: Set up Flux CLI uses: fluxcd/flux2/action@5350425cdcd5fa015337e09fa502153c0275bd4b # v2.4.0 @@ -969,7 +969,7 @@ jobs: version: "2.2.1" - name: Get Cluster Credentials - uses: google-github-actions/get-gke-credentials@6051de21ad50fbb1767bc93c11357a49082ad116 # v2.2.1 + uses: google-github-actions/get-gke-credentials@206d64b64b0eba0a6e2f25113d044c31776ca8d6 # v2.2.2 with: cluster_name: dogfood-v2 location: us-central1-a diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml index f968d29ce13f1..b440e299885db 100644 --- a/.github/workflows/dogfood.yaml +++ b/.github/workflows/dogfood.yaml @@ -100,7 +100,7 @@ jobs: uses: ./.github/actions/setup-tf - name: Authenticate to Google Cloud - uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 74b5b7b35a1e7..cf4fb87cd0673 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -192,14 +192,14 @@ jobs: # Setup GCloud for signing Windows binaries. - name: Authenticate to Google Cloud id: gcloud_auth - uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }} service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }} token_format: "access_token" - name: Setup GCloud SDK - uses: google-github-actions/setup-gcloud@f0990588f1e5b5af6827153b93673613abdc6ec7 # v2.1.1 + uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # v2.1.2 - name: Build binaries run: | @@ -365,13 +365,13 @@ jobs: CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }} - name: Authenticate to Google Cloud - uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_ID_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - name: Setup GCloud SDK - uses: google-github-actions/setup-gcloud@f0990588f1e5b5af6827153b93673613abdc6ec7 # 2.1.1 + uses: google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a # 2.1.2 - name: Publish Helm Chart if: ${{ !inputs.dry_run }} diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index b384068e831f2..e388502a0c0d9 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -23,6 +23,7 @@ EDE = "EDE" # HELO is an SMTP command HELO = "HELO" LKE = "LKE" +byt = "byt" [files] extend-exclude = [ diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index be17abe8dd63b..2141eddf111eb 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -941,7 +941,7 @@ func minTime(t, u time.Time) time.Time { return u } -func provisonerJobStatus(j database.ProvisionerJob) database.ProvisionerJobStatus { +func provisionerJobStatus(j database.ProvisionerJob) database.ProvisionerJobStatus { if isNotNull(j.CompletedAt) { if j.Error.String != "" { return database.ProvisionerJobStatusFailed @@ -1202,7 +1202,7 @@ func (q *FakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu provisionerJob.StartedAt = arg.StartedAt provisionerJob.UpdatedAt = arg.StartedAt.Time provisionerJob.WorkerID = arg.WorkerID - provisionerJob.JobStatus = provisonerJobStatus(provisionerJob) + provisionerJob.JobStatus = provisionerJobStatus(provisionerJob) q.provisionerJobs[index] = provisionerJob // clone the Tags before returning, since maps are reference types and // we don't want the caller to be able to mutate the map we have inside @@ -7456,7 +7456,7 @@ func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.Inser Tags: maps.Clone(arg.Tags), TraceMetadata: arg.TraceMetadata, } - job.JobStatus = provisonerJobStatus(job) + job.JobStatus = provisionerJobStatus(job) q.provisionerJobs = append(q.provisionerJobs, job) return job, nil } @@ -8842,7 +8842,7 @@ func (q *FakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.U continue } job.UpdatedAt = arg.UpdatedAt - job.JobStatus = provisonerJobStatus(job) + job.JobStatus = provisionerJobStatus(job) q.provisionerJobs[index] = job return nil } @@ -8863,7 +8863,7 @@ func (q *FakeQuerier) UpdateProvisionerJobWithCancelByID(_ context.Context, arg } job.CanceledAt = arg.CanceledAt job.CompletedAt = arg.CompletedAt - job.JobStatus = provisonerJobStatus(job) + job.JobStatus = provisionerJobStatus(job) q.provisionerJobs[index] = job return nil } @@ -8886,7 +8886,7 @@ func (q *FakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar job.CompletedAt = arg.CompletedAt job.Error = arg.Error job.ErrorCode = arg.ErrorCode - job.JobStatus = provisonerJobStatus(job) + job.JobStatus = provisionerJobStatus(job) q.provisionerJobs[index] = job return nil } diff --git a/coderd/httpmw/workspaceproxy.go b/coderd/httpmw/workspaceproxy.go index 8ee53187850d0..1f2de1ed46160 100644 --- a/coderd/httpmw/workspaceproxy.go +++ b/coderd/httpmw/workspaceproxy.go @@ -148,7 +148,7 @@ func ExtractWorkspaceProxy(opts ExtractWorkspaceProxyConfig) func(http.Handler) type workspaceProxyParamContextKey struct{} -// WorkspaceProxyParam returns the worksace proxy from the ExtractWorkspaceProxyParam handler. +// WorkspaceProxyParam returns the workspace proxy from the ExtractWorkspaceProxyParam handler. func WorkspaceProxyParam(r *http.Request) database.WorkspaceProxy { user, ok := r.Context().Value(workspaceProxyParamContextKey{}).(database.WorkspaceProxy) if !ok { diff --git a/coderd/unhanger/detector.go b/coderd/unhanger/detector.go index 9a3440f705ed7..14383b1839363 100644 --- a/coderd/unhanger/detector.go +++ b/coderd/unhanger/detector.go @@ -57,14 +57,14 @@ func (acquireLockError) Error() string { return "lock is held by another client" } -// jobInelligibleError is returned when a job is not eligible to be terminated +// jobIneligibleError is returned when a job is not eligible to be terminated // anymore. -type jobInelligibleError struct { +type jobIneligibleError struct { Err error } // Error implements error. -func (e jobInelligibleError) Error() string { +func (e jobIneligibleError) Error() string { return fmt.Sprintf("job is no longer eligible to be terminated: %s", e.Err) } @@ -198,7 +198,7 @@ func (d *Detector) run(t time.Time) Stats { err := unhangJob(ctx, log, d.db, d.pubsub, job.ID) if err != nil { - if !(xerrors.As(err, &acquireLockError{}) || xerrors.As(err, &jobInelligibleError{})) { + if !(xerrors.As(err, &acquireLockError{}) || xerrors.As(err, &jobIneligibleError{})) { log.Error(ctx, "error forcefully terminating hung provisioner job", slog.Error(err)) } continue @@ -233,17 +233,17 @@ func unhangJob(ctx context.Context, log slog.Logger, db database.Store, pub pubs if !job.StartedAt.Valid { // This shouldn't be possible to hit because the query only selects // started and not completed jobs, and a job can't be "un-started". - return jobInelligibleError{ + return jobIneligibleError{ Err: xerrors.New("job is not started"), } } if job.CompletedAt.Valid { - return jobInelligibleError{ + return jobIneligibleError{ Err: xerrors.Errorf("job is completed (status %s)", job.JobStatus), } } if job.UpdatedAt.After(time.Now().Add(-HungJobDuration)) { - return jobInelligibleError{ + return jobIneligibleError{ Err: xerrors.New("job has been updated recently"), } } From 886dcbec843766da9e459557335ccdf5dbea7ac6 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 5 Nov 2024 13:50:10 +0400 Subject: [PATCH 063/223] chore: refactor coordination (#15343) Refactors the way clients of the Tailnet API (clients of the API, which include both workspace "agents" and "clients") interact with the API. Introduces the idea of abstract "controllers" for each of the RPCs in the API, and implements a Coordination controller by refactoring from `workspacesdk`. chore re: #14729 --- agent/agent.go | 5 +- agent/agent_test.go | 13 +- agent/agenttest/client.go | 2 - codersdk/workspacesdk/connector.go | 6 +- tailnet/controllers.go | 361 ++++++++++++++++++++++++ tailnet/controllers_test.go | 283 +++++++++++++++++++ tailnet/coordinator.go | 296 ------------------- tailnet/coordinator_test.go | 267 ------------------ tailnet/test/integration/integration.go | 3 +- 9 files changed, 658 insertions(+), 578 deletions(-) create mode 100644 tailnet/controllers.go create mode 100644 tailnet/controllers_test.go diff --git a/agent/agent.go b/agent/agent.go index cb0037dd0ed48..4c8497d105acc 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1352,7 +1352,8 @@ func (a *agent) runCoordinator(ctx context.Context, conn drpc.Conn, network *tai defer close(disconnected) a.closeMutex.Unlock() - coordination := tailnet.NewRemoteCoordination(a.logger, coordinate, network, uuid.Nil) + ctrl := tailnet.NewAgentCoordinationController(a.logger, network) + coordination := ctrl.New(coordinate) errCh := make(chan error, 1) go func() { @@ -1364,7 +1365,7 @@ func (a *agent) runCoordinator(ctx context.Context, conn drpc.Conn, network *tai a.logger.Warn(ctx, "failed to close remote coordination", slog.Error(err)) } return - case err := <-coordination.Error(): + case err := <-coordination.Wait(): errCh <- err } }() diff --git a/agent/agent_test.go b/agent/agent_test.go index addae8c3d897d..e7fd753b8d020 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1918,10 +1918,8 @@ func TestAgent_UpdatedDERP(t *testing.T) { testCtx, testCtxCancel := context.WithCancel(context.Background()) t.Cleanup(testCtxCancel) clientID := uuid.New() - coordination := tailnet.NewInMemoryCoordination( - testCtx, logger, - clientID, agentID, - coordinator, conn) + ctrl := tailnet.NewSingleDestController(logger, conn, agentID) + coordination := ctrl.New(tailnet.NewInMemoryCoordinatorClient(logger, clientID, agentID, coordinator)) t.Cleanup(func() { t.Logf("closing coordination %s", name) cctx, ccancel := context.WithTimeout(testCtx, testutil.WaitShort) @@ -2409,10 +2407,9 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati testCtx, testCtxCancel := context.WithCancel(context.Background()) t.Cleanup(testCtxCancel) clientID := uuid.New() - coordination := tailnet.NewInMemoryCoordination( - testCtx, logger, - clientID, metadata.AgentID, - coordinator, conn) + ctrl := tailnet.NewSingleDestController(logger, conn, metadata.AgentID) + coordination := ctrl.New(tailnet.NewInMemoryCoordinatorClient( + logger, clientID, metadata.AgentID, coordinator)) t.Cleanup(func() { cctx, ccancel := context.WithTimeout(testCtx, testutil.WaitShort) defer ccancel() diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index a17f9200a9b87..8817b311fcda6 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -71,7 +71,6 @@ func NewClient(t testing.TB, t: t, logger: logger.Named("client"), agentID: agentID, - coordinator: coordinator, server: server, fakeAgentAPI: fakeAAPI, derpMapUpdates: derpMapUpdates, @@ -82,7 +81,6 @@ type Client struct { t testing.TB logger slog.Logger agentID uuid.UUID - coordinator tailnet.Coordinator server *drpcserver.Server fakeAgentAPI *FakeAgentAPI LastWorkspaceAgent func() diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index 780478e91a55f..eb14b34519482 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -66,6 +66,7 @@ type tailnetAPIConnector struct { clock quartz.Clock dialOptions *websocket.DialOptions conn tailnetConn + coordCtrl tailnet.CoordinationController customDialFn func() (proto.DRPCTailnetClient, error) clientMu sync.RWMutex @@ -112,6 +113,7 @@ func (tac *tailnetAPIConnector) manageGracefulTimeout() { // Runs a tailnetAPIConnector using the provided connection func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { tac.conn = conn + tac.coordCtrl = tailnet.NewSingleDestController(tac.logger, conn, tac.agentID) tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) go tac.manageGracefulTimeout() go func() { @@ -272,7 +274,7 @@ func (tac *tailnetAPIConnector) coordinate(client proto.DRPCTailnetClient) { tac.logger.Debug(tac.ctx, "error closing Coordinate RPC", slog.Error(cErr)) } }() - coordination := tailnet.NewRemoteCoordination(tac.logger, coord, tac.conn, tac.agentID) + coordination := tac.coordCtrl.New(coord) tac.logger.Debug(tac.ctx, "serving coordinator") select { case <-tac.ctx.Done(): @@ -281,7 +283,7 @@ func (tac *tailnetAPIConnector) coordinate(client proto.DRPCTailnetClient) { if crdErr != nil { tac.logger.Warn(tac.ctx, "failed to close remote coordination", slog.Error(err)) } - case err = <-coordination.Error(): + case err = <-coordination.Wait(): if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) && diff --git a/tailnet/controllers.go b/tailnet/controllers.go new file mode 100644 index 0000000000000..84a9bd4d79597 --- /dev/null +++ b/tailnet/controllers.go @@ -0,0 +1,361 @@ +package tailnet + +import ( + "context" + "fmt" + "io" + "sync" + + "github.com/google/uuid" + "golang.org/x/xerrors" + "storj.io/drpc" + "tailscale.com/tailcfg" + + "cdr.dev/slog" + "github.com/coder/coder/v2/tailnet/proto" +) + +// A Controller connects to the tailnet control plane, and then uses the control protocols to +// program a tailnet.Conn in production (in test it could be an interface simulating the Conn). It +// delegates this task to sub-controllers responsible for the main areas of the tailnet control +// protocol: coordination, DERP map updates, resume tokens, and telemetry. +type Controller struct { + Dialer ControlProtocolDialer + CoordCtrl CoordinationController + DERPCtrl DERPController + ResumeTokenCtrl ResumeTokenController + TelemetryCtrl TelemetryController +} + +type CloserWaiter interface { + Close(context.Context) error + Wait() <-chan error +} + +// CoordinatorClient is an abstraction of the Coordinator's control protocol interface from the +// perspective of a protocol client (i.e. the Coder Agent is also a client of this interface). +type CoordinatorClient interface { + Close() error + Send(*proto.CoordinateRequest) error + Recv() (*proto.CoordinateResponse, error) +} + +// A CoordinationController accepts connections to the control plane, and handles the Coordination +// protocol on behalf of some Coordinatee (tailnet.Conn in production). This is the "glue" code +// between them. +type CoordinationController interface { + New(CoordinatorClient) CloserWaiter +} + +// DERPClient is an abstraction of the stream of DERPMap updates from the control plane. +type DERPClient interface { + Close() error + Recv() (*tailcfg.DERPMap, error) +} + +// A DERPController accepts connections to the control plane, and handles the DERPMap updates +// delivered over them by programming the data plane (tailnet.Conn or some test interface). +type DERPController interface { + New(DERPClient) CloserWaiter +} + +type ResumeTokenClient interface { + RefreshResumeToken(ctx context.Context, in *proto.RefreshResumeTokenRequest) (*proto.RefreshResumeTokenResponse, error) +} + +type ResumeTokenController interface { + New(ResumeTokenClient) CloserWaiter + Token() (string, bool) +} + +type TelemetryClient interface { + PostTelemetry(ctx context.Context, in *proto.TelemetryRequest) (*proto.TelemetryResponse, error) +} + +type TelemetryController interface { + New(TelemetryClient) +} + +// ControlProtocolClients represents an abstract interface to the tailnet control plane via a set +// of protocol clients. The Closer should close all the clients (e.g. by closing the underlying +// connection). +type ControlProtocolClients struct { + Closer io.Closer + Coordinator CoordinatorClient + DERP DERPClient + ResumeToken ResumeTokenClient + Telemetry TelemetryClient +} + +type ControlProtocolDialer interface { + // Dial connects to the tailnet control plane and returns clients for the different control + // sub-protocols (coordination, DERP maps, resume tokens, and telemetry). If the + // ResumeTokenController is not nil, the dialer should query for a resume token and use it to + // dial, if available. + Dial(ctx context.Context, r ResumeTokenController) (ControlProtocolClients, error) +} + +// basicCoordinationController handles the basic coordination operations common to all types of +// tailnet consumers: +// +// 1. sending local node updates to the Coordinator +// 2. receiving peer node updates and programming them into the Coordinatee (e.g. tailnet.Conn) +// 3. (optionally) sending ReadyToHandshake acknowledgements for peer updates. +type basicCoordinationController struct { + logger slog.Logger + coordinatee Coordinatee + sendAcks bool +} + +func (c *basicCoordinationController) New(client CoordinatorClient) CloserWaiter { + b := &basicCoordination{ + logger: c.logger, + errChan: make(chan error, 1), + coordinatee: c.coordinatee, + client: client, + respLoopDone: make(chan struct{}), + sendAcks: c.sendAcks, + } + + c.coordinatee.SetNodeCallback(func(node *Node) { + pn, err := NodeToProto(node) + if err != nil { + b.logger.Critical(context.Background(), "failed to convert node", slog.Error(err)) + b.sendErr(err) + return + } + b.Lock() + defer b.Unlock() + if b.closed { + b.logger.Debug(context.Background(), "ignored node update because coordination is closed") + return + } + err = b.client.Send(&proto.CoordinateRequest{UpdateSelf: &proto.CoordinateRequest_UpdateSelf{Node: pn}}) + if err != nil { + b.sendErr(xerrors.Errorf("write: %w", err)) + } + }) + go b.respLoop() + + return b +} + +type basicCoordination struct { + sync.Mutex + closed bool + errChan chan error + coordinatee Coordinatee + logger slog.Logger + client CoordinatorClient + respLoopDone chan struct{} + sendAcks bool +} + +func (c *basicCoordination) Close(ctx context.Context) (retErr error) { + c.Lock() + defer c.Unlock() + if c.closed { + return nil + } + c.closed = true + defer func() { + // We shouldn't just close the protocol right away, because the way dRPC streams work is + // that if you close them, that could take effect immediately, even before the Disconnect + // message is processed. Coordinators are supposed to hang up on us once they get a + // Disconnect message, so we should wait around for that until the context expires. + select { + case <-c.respLoopDone: + c.logger.Debug(ctx, "responses closed after disconnect") + return + case <-ctx.Done(): + c.logger.Warn(ctx, "context expired while waiting for coordinate responses to close") + } + // forcefully close the stream + protoErr := c.client.Close() + <-c.respLoopDone + if retErr == nil { + retErr = protoErr + } + }() + err := c.client.Send(&proto.CoordinateRequest{Disconnect: &proto.CoordinateRequest_Disconnect{}}) + if err != nil && !xerrors.Is(err, io.EOF) { + // Coordinator RPC hangs up when it gets disconnect, so EOF is expected. + return xerrors.Errorf("send disconnect: %w", err) + } + c.logger.Debug(context.Background(), "sent disconnect") + return nil +} + +func (c *basicCoordination) Wait() <-chan error { + return c.errChan +} + +func (c *basicCoordination) sendErr(err error) { + select { + case c.errChan <- err: + default: + } +} + +func (c *basicCoordination) respLoop() { + defer func() { + cErr := c.client.Close() + if cErr != nil { + c.logger.Debug(context.Background(), "failed to close coordinate client after respLoop exit", slog.Error(cErr)) + } + c.coordinatee.SetAllPeersLost() + close(c.respLoopDone) + }() + for { + resp, err := c.client.Recv() + if err != nil { + c.logger.Debug(context.Background(), "failed to read from protocol", slog.Error(err)) + c.sendErr(xerrors.Errorf("read: %w", err)) + return + } + + err = c.coordinatee.UpdatePeers(resp.GetPeerUpdates()) + if err != nil { + c.logger.Debug(context.Background(), "failed to update peers", slog.Error(err)) + c.sendErr(xerrors.Errorf("update peers: %w", err)) + return + } + + // Only send ReadyForHandshake acks from peers without a target. + if c.sendAcks { + // Send an ack back for all received peers. This could + // potentially be smarter to only send an ACK once per client, + // but there's nothing currently stopping clients from reusing + // IDs. + rfh := []*proto.CoordinateRequest_ReadyForHandshake{} + for _, peer := range resp.GetPeerUpdates() { + if peer.Kind != proto.CoordinateResponse_PeerUpdate_NODE { + continue + } + + rfh = append(rfh, &proto.CoordinateRequest_ReadyForHandshake{Id: peer.Id}) + } + if len(rfh) > 0 { + err := c.client.Send(&proto.CoordinateRequest{ + ReadyForHandshake: rfh, + }) + if err != nil { + c.logger.Debug(context.Background(), "failed to send ready for handshake", slog.Error(err)) + c.sendErr(xerrors.Errorf("send: %w", err)) + return + } + } + } + } +} + +type singleDestController struct { + *basicCoordinationController + dest uuid.UUID +} + +// NewSingleDestController creates a CoordinationController for Coder clients that connect to a +// single tunnel destination, e.g. `coder ssh`, which connects to a single workspace Agent. +func NewSingleDestController(logger slog.Logger, coordinatee Coordinatee, dest uuid.UUID) CoordinationController { + coordinatee.SetTunnelDestination(dest) + return &singleDestController{ + basicCoordinationController: &basicCoordinationController{ + logger: logger, + coordinatee: coordinatee, + sendAcks: false, + }, + dest: dest, + } +} + +func (c *singleDestController) New(client CoordinatorClient) CloserWaiter { + // nolint: forcetypeassert + b := c.basicCoordinationController.New(client).(*basicCoordination) + err := client.Send(&proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: c.dest[:]}}) + if err != nil { + b.sendErr(err) + } + return b +} + +// NewAgentCoordinationController creates a CoordinationController for Coder Agents, which never +// create tunnels and always send ReadyToHandshake acknowledgements. +func NewAgentCoordinationController(logger slog.Logger, coordinatee Coordinatee) CoordinationController { + return &basicCoordinationController{ + logger: logger, + coordinatee: coordinatee, + sendAcks: true, + } +} + +type inMemoryCoordClient struct { + sync.Mutex + ctx context.Context + cancel context.CancelFunc + closed bool + logger slog.Logger + resps <-chan *proto.CoordinateResponse + reqs chan<- *proto.CoordinateRequest +} + +func (c *inMemoryCoordClient) Close() error { + c.cancel() + c.Lock() + defer c.Unlock() + if c.closed { + return nil + } + c.closed = true + close(c.reqs) + return nil +} + +func (c *inMemoryCoordClient) Send(request *proto.CoordinateRequest) error { + c.Lock() + defer c.Unlock() + if c.closed { + return drpc.ClosedError.New("in-memory coordinator client closed") + } + select { + case c.reqs <- request: + return nil + case <-c.ctx.Done(): + return drpc.ClosedError.New("in-memory coordinator client closed") + } +} + +func (c *inMemoryCoordClient) Recv() (*proto.CoordinateResponse, error) { + select { + case resp, ok := <-c.resps: + if ok { + return resp, nil + } + // response from Coordinator was closed, so close the send direction as well, so that the + // Coordinator won't be waiting for us while shutting down. + _ = c.Close() + return nil, io.EOF + case <-c.ctx.Done(): + return nil, drpc.ClosedError.New("in-memory coord client closed") + } +} + +// NewInMemoryCoordinatorClient creates a coordination client that uses channels to connect to a +// local Coordinator. (The typical alternative is a DRPC-based client.) +func NewInMemoryCoordinatorClient( + logger slog.Logger, + clientID, agentID uuid.UUID, + coordinator Coordinator, +) CoordinatorClient { + logger = logger.With(slog.F("agent_id", agentID), slog.F("client_id", clientID)) + auth := ClientCoordinateeAuth{AgentID: agentID} + c := &inMemoryCoordClient{logger: logger} + c.ctx, c.cancel = context.WithCancel(context.Background()) + + // use the background context since we will depend exclusively on closing the req channel to + // tell the coordinator we are done. + c.reqs, c.resps = coordinator.Coordinate(context.Background(), + clientID, fmt.Sprintf("inmemory%s", clientID), + auth, + ) + return c +} diff --git a/tailnet/controllers_test.go b/tailnet/controllers_test.go new file mode 100644 index 0000000000000..2e3098f80fff9 --- /dev/null +++ b/tailnet/controllers_test.go @@ -0,0 +1,283 @@ +package tailnet_test + +import ( + "context" + "io" + "net" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "tailscale.com/tailcfg" + "tailscale.com/types/key" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/tailnet" + "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/coder/v2/tailnet/tailnettest" + "github.com/coder/coder/v2/testutil" +) + +func TestInMemoryCoordination(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + clientID := uuid.UUID{1} + agentID := uuid.UUID{2} + mCoord := tailnettest.NewMockCoordinator(gomock.NewController(t)) + fConn := &fakeCoordinatee{} + + reqs := make(chan *proto.CoordinateRequest, 100) + resps := make(chan *proto.CoordinateResponse, 100) + mCoord.EXPECT().Coordinate(gomock.Any(), clientID, gomock.Any(), tailnet.ClientCoordinateeAuth{agentID}). + Times(1).Return(reqs, resps) + + ctrl := tailnet.NewSingleDestController(logger, fConn, agentID) + uut := ctrl.New(tailnet.NewInMemoryCoordinatorClient(logger, clientID, agentID, mCoord)) + defer uut.Close(ctx) + + coordinationTest(ctx, t, uut, fConn, reqs, resps, agentID) + + // Recv loop should be terminated by the server hanging up after Disconnect + err := testutil.RequireRecvCtx(ctx, t, uut.Wait()) + require.ErrorIs(t, err, io.EOF) +} + +func TestSingleDestController(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + clientID := uuid.UUID{1} + agentID := uuid.UUID{2} + mCoord := tailnettest.NewMockCoordinator(gomock.NewController(t)) + fConn := &fakeCoordinatee{} + + reqs := make(chan *proto.CoordinateRequest, 100) + resps := make(chan *proto.CoordinateResponse, 100) + mCoord.EXPECT().Coordinate(gomock.Any(), clientID, gomock.Any(), tailnet.ClientCoordinateeAuth{agentID}). + Times(1).Return(reqs, resps) + + var coord tailnet.Coordinator = mCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger.Named("svc"), + CoordPtr: &coordPtr, + DERPMapUpdateFrequency: time.Hour, + DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, + NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), + }) + require.NoError(t, err) + sC, cC := net.Pipe() + + serveErr := make(chan error, 1) + go func() { + err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: agentID, + }, + }) + serveErr <- err + }() + + client, err := tailnet.NewDRPCClient(cC, logger) + require.NoError(t, err) + protocol, err := client.Coordinate(ctx) + require.NoError(t, err) + + ctrl := tailnet.NewSingleDestController(logger.Named("coordination"), fConn, agentID) + uut := ctrl.New(protocol) + defer uut.Close(ctx) + + coordinationTest(ctx, t, uut, fConn, reqs, resps, agentID) + + // Recv loop should be terminated by the server hanging up after Disconnect + err = testutil.RequireRecvCtx(ctx, t, uut.Wait()) + require.ErrorIs(t, err, io.EOF) +} + +func TestAgentCoordinationController_SendsReadyForHandshake(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + clientID := uuid.UUID{1} + agentID := uuid.UUID{2} + mCoord := tailnettest.NewMockCoordinator(gomock.NewController(t)) + fConn := &fakeCoordinatee{} + + reqs := make(chan *proto.CoordinateRequest, 100) + resps := make(chan *proto.CoordinateResponse, 100) + mCoord.EXPECT().Coordinate(gomock.Any(), clientID, gomock.Any(), tailnet.ClientCoordinateeAuth{agentID}). + Times(1).Return(reqs, resps) + + var coord tailnet.Coordinator = mCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger.Named("svc"), + CoordPtr: &coordPtr, + DERPMapUpdateFrequency: time.Hour, + DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, + NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), + }) + require.NoError(t, err) + sC, cC := net.Pipe() + + serveErr := make(chan error, 1) + go func() { + err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, tailnet.StreamID{ + Name: "client", + ID: clientID, + Auth: tailnet.ClientCoordinateeAuth{ + AgentID: agentID, + }, + }) + serveErr <- err + }() + + client, err := tailnet.NewDRPCClient(cC, logger) + require.NoError(t, err) + protocol, err := client.Coordinate(ctx) + require.NoError(t, err) + + ctrl := tailnet.NewAgentCoordinationController(logger.Named("coordination"), fConn) + uut := ctrl.New(protocol) + defer uut.Close(ctx) + + nk, err := key.NewNode().Public().MarshalBinary() + require.NoError(t, err) + dk, err := key.NewDisco().Public().MarshalText() + require.NoError(t, err) + testutil.RequireSendCtx(ctx, t, resps, &proto.CoordinateResponse{ + PeerUpdates: []*proto.CoordinateResponse_PeerUpdate{{ + Id: clientID[:], + Kind: proto.CoordinateResponse_PeerUpdate_NODE, + Node: &proto.Node{ + Id: 3, + Key: nk, + Disco: string(dk), + }, + }}, + }) + + rfh := testutil.RequireRecvCtx(ctx, t, reqs) + require.NotNil(t, rfh.ReadyForHandshake) + require.Len(t, rfh.ReadyForHandshake, 1) + require.Equal(t, clientID[:], rfh.ReadyForHandshake[0].Id) + + go uut.Close(ctx) + dis := testutil.RequireRecvCtx(ctx, t, reqs) + require.NotNil(t, dis) + require.NotNil(t, dis.Disconnect) + close(resps) + + // Recv loop should be terminated by the server hanging up after Disconnect + err = testutil.RequireRecvCtx(ctx, t, uut.Wait()) + require.ErrorIs(t, err, io.EOF) +} + +// coordinationTest tests that a coordination behaves correctly +func coordinationTest( + ctx context.Context, t *testing.T, + uut tailnet.CloserWaiter, fConn *fakeCoordinatee, + reqs chan *proto.CoordinateRequest, resps chan *proto.CoordinateResponse, + agentID uuid.UUID, +) { + // It should add the tunnel, since we configured as a client + req := testutil.RequireRecvCtx(ctx, t, reqs) + require.Equal(t, agentID[:], req.GetAddTunnel().GetId()) + + // when we call the callback, it should send a node update + require.NotNil(t, fConn.callback) + fConn.callback(&tailnet.Node{PreferredDERP: 1}) + + req = testutil.RequireRecvCtx(ctx, t, reqs) + require.Equal(t, int32(1), req.GetUpdateSelf().GetNode().GetPreferredDerp()) + + // When we send a peer update, it should update the coordinatee + nk, err := key.NewNode().Public().MarshalBinary() + require.NoError(t, err) + dk, err := key.NewDisco().Public().MarshalText() + require.NoError(t, err) + updates := []*proto.CoordinateResponse_PeerUpdate{ + { + Id: agentID[:], + Kind: proto.CoordinateResponse_PeerUpdate_NODE, + Node: &proto.Node{ + Id: 2, + Key: nk, + Disco: string(dk), + }, + }, + } + testutil.RequireSendCtx(ctx, t, resps, &proto.CoordinateResponse{PeerUpdates: updates}) + require.Eventually(t, func() bool { + fConn.Lock() + defer fConn.Unlock() + return len(fConn.updates) > 0 + }, testutil.WaitShort, testutil.IntervalFast) + require.Len(t, fConn.updates[0], 1) + require.Equal(t, agentID[:], fConn.updates[0][0].Id) + + errCh := make(chan error, 1) + go func() { + errCh <- uut.Close(ctx) + }() + + // When we close, it should gracefully disconnect + req = testutil.RequireRecvCtx(ctx, t, reqs) + require.NotNil(t, req.Disconnect) + close(resps) + + err = testutil.RequireRecvCtx(ctx, t, errCh) + require.NoError(t, err) + + // It should set all peers lost on the coordinatee + require.Equal(t, 1, fConn.setAllPeersLostCalls) +} + +type fakeCoordinatee struct { + sync.Mutex + callback func(*tailnet.Node) + updates [][]*proto.CoordinateResponse_PeerUpdate + setAllPeersLostCalls int + tunnelDestinations map[uuid.UUID]struct{} +} + +func (f *fakeCoordinatee) UpdatePeers(updates []*proto.CoordinateResponse_PeerUpdate) error { + f.Lock() + defer f.Unlock() + f.updates = append(f.updates, updates) + return nil +} + +func (f *fakeCoordinatee) SetAllPeersLost() { + f.Lock() + defer f.Unlock() + f.setAllPeersLostCalls++ +} + +func (f *fakeCoordinatee) SetTunnelDestination(id uuid.UUID) { + f.Lock() + defer f.Unlock() + + if f.tunnelDestinations == nil { + f.tunnelDestinations = map[uuid.UUID]struct{}{} + } + f.tunnelDestinations[id] = struct{}{} +} + +func (f *fakeCoordinatee) SetNodeCallback(callback func(*tailnet.Node)) { + f.Lock() + defer f.Unlock() + f.callback = callback +} diff --git a/tailnet/coordinator.go b/tailnet/coordinator.go index b0592598959f3..d883ca1b4c483 100644 --- a/tailnet/coordinator.go +++ b/tailnet/coordinator.go @@ -90,302 +90,6 @@ type Coordinatee interface { SetTunnelDestination(id uuid.UUID) } -type Coordination interface { - Close(context.Context) error - Error() <-chan error -} - -type remoteCoordination struct { - sync.Mutex - closed bool - errChan chan error - coordinatee Coordinatee - tgt uuid.UUID - logger slog.Logger - protocol proto.DRPCTailnet_CoordinateClient - respLoopDone chan struct{} -} - -// Close attempts to gracefully close the remoteCoordination by sending a Disconnect message and -// waiting for the server to hang up the coordination. If the provided context expires, we stop -// waiting for the server and close the coordination stream from our end. -func (c *remoteCoordination) Close(ctx context.Context) (retErr error) { - c.Lock() - defer c.Unlock() - if c.closed { - return nil - } - c.closed = true - defer func() { - // We shouldn't just close the protocol right away, because the way dRPC streams work is - // that if you close them, that could take effect immediately, even before the Disconnect - // message is processed. Coordinators are supposed to hang up on us once they get a - // Disconnect message, so we should wait around for that until the context expires. - select { - case <-c.respLoopDone: - c.logger.Debug(ctx, "responses closed after disconnect") - return - case <-ctx.Done(): - c.logger.Warn(ctx, "context expired while waiting for coordinate responses to close") - } - // forcefully close the stream - protoErr := c.protocol.Close() - <-c.respLoopDone - if retErr == nil { - retErr = protoErr - } - }() - err := c.protocol.Send(&proto.CoordinateRequest{Disconnect: &proto.CoordinateRequest_Disconnect{}}) - if err != nil && !xerrors.Is(err, io.EOF) { - // Coordinator RPC hangs up when it gets disconnect, so EOF is expected. - return xerrors.Errorf("send disconnect: %w", err) - } - c.logger.Debug(context.Background(), "sent disconnect") - return nil -} - -func (c *remoteCoordination) Error() <-chan error { - return c.errChan -} - -func (c *remoteCoordination) sendErr(err error) { - select { - case c.errChan <- err: - default: - } -} - -func (c *remoteCoordination) respLoop() { - defer func() { - c.coordinatee.SetAllPeersLost() - close(c.respLoopDone) - }() - for { - resp, err := c.protocol.Recv() - if err != nil { - c.logger.Debug(context.Background(), "failed to read from protocol", slog.Error(err)) - c.sendErr(xerrors.Errorf("read: %w", err)) - return - } - - err = c.coordinatee.UpdatePeers(resp.GetPeerUpdates()) - if err != nil { - c.logger.Debug(context.Background(), "failed to update peers", slog.Error(err)) - c.sendErr(xerrors.Errorf("update peers: %w", err)) - return - } - - // Only send acks from peers without a target. - if c.tgt == uuid.Nil { - // Send an ack back for all received peers. This could - // potentially be smarter to only send an ACK once per client, - // but there's nothing currently stopping clients from reusing - // IDs. - rfh := []*proto.CoordinateRequest_ReadyForHandshake{} - for _, peer := range resp.GetPeerUpdates() { - if peer.Kind != proto.CoordinateResponse_PeerUpdate_NODE { - continue - } - - rfh = append(rfh, &proto.CoordinateRequest_ReadyForHandshake{Id: peer.Id}) - } - if len(rfh) > 0 { - err := c.protocol.Send(&proto.CoordinateRequest{ - ReadyForHandshake: rfh, - }) - if err != nil { - c.logger.Debug(context.Background(), "failed to send ready for handshake", slog.Error(err)) - c.sendErr(xerrors.Errorf("send: %w", err)) - return - } - } - } - } -} - -// NewRemoteCoordination uses the provided protocol to coordinate the provided coordinatee (usually a -// Conn). If the tunnelTarget is not uuid.Nil, then we add a tunnel to the peer (i.e. we are acting as -// a client---agents should NOT set this!). -func NewRemoteCoordination(logger slog.Logger, - protocol proto.DRPCTailnet_CoordinateClient, coordinatee Coordinatee, - tunnelTarget uuid.UUID, -) Coordination { - c := &remoteCoordination{ - errChan: make(chan error, 1), - coordinatee: coordinatee, - tgt: tunnelTarget, - logger: logger, - protocol: protocol, - respLoopDone: make(chan struct{}), - } - if tunnelTarget != uuid.Nil { - c.coordinatee.SetTunnelDestination(tunnelTarget) - c.Lock() - err := c.protocol.Send(&proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: tunnelTarget[:]}}) - c.Unlock() - if err != nil { - c.sendErr(err) - } - } - - coordinatee.SetNodeCallback(func(node *Node) { - pn, err := NodeToProto(node) - if err != nil { - c.logger.Critical(context.Background(), "failed to convert node", slog.Error(err)) - c.sendErr(err) - return - } - c.Lock() - defer c.Unlock() - if c.closed { - c.logger.Debug(context.Background(), "ignored node update because coordination is closed") - return - } - err = c.protocol.Send(&proto.CoordinateRequest{UpdateSelf: &proto.CoordinateRequest_UpdateSelf{Node: pn}}) - if err != nil { - c.sendErr(xerrors.Errorf("write: %w", err)) - } - }) - go c.respLoop() - return c -} - -type inMemoryCoordination struct { - sync.Mutex - ctx context.Context - errChan chan error - closed bool - respLoopDone chan struct{} - coordinatee Coordinatee - logger slog.Logger - resps <-chan *proto.CoordinateResponse - reqs chan<- *proto.CoordinateRequest -} - -func (c *inMemoryCoordination) sendErr(err error) { - select { - case c.errChan <- err: - default: - } -} - -func (c *inMemoryCoordination) Error() <-chan error { - return c.errChan -} - -// NewInMemoryCoordination connects a Coordinatee (usually Conn) to an in memory Coordinator, for testing -// or local clients. Set ClientID to uuid.Nil for an agent. -func NewInMemoryCoordination( - ctx context.Context, logger slog.Logger, - clientID, agentID uuid.UUID, - coordinator Coordinator, coordinatee Coordinatee, -) Coordination { - thisID := agentID - logger = logger.With(slog.F("agent_id", agentID)) - var auth CoordinateeAuth = AgentCoordinateeAuth{ID: agentID} - if clientID != uuid.Nil { - // this is a client connection - auth = ClientCoordinateeAuth{AgentID: agentID} - logger = logger.With(slog.F("client_id", clientID)) - thisID = clientID - } - c := &inMemoryCoordination{ - ctx: ctx, - errChan: make(chan error, 1), - coordinatee: coordinatee, - logger: logger, - respLoopDone: make(chan struct{}), - } - - // use the background context since we will depend exclusively on closing the req channel to - // tell the coordinator we are done. - c.reqs, c.resps = coordinator.Coordinate(context.Background(), - thisID, fmt.Sprintf("inmemory%s", thisID), - auth, - ) - go c.respLoop() - if agentID != uuid.Nil { - select { - case <-ctx.Done(): - c.logger.Warn(ctx, "context expired before we could add tunnel", slog.Error(ctx.Err())) - return c - case c.reqs <- &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: agentID[:]}}: - // OK! - } - } - coordinatee.SetNodeCallback(func(n *Node) { - pn, err := NodeToProto(n) - if err != nil { - c.logger.Critical(ctx, "failed to convert node", slog.Error(err)) - c.sendErr(err) - return - } - c.Lock() - defer c.Unlock() - if c.closed { - return - } - select { - case <-ctx.Done(): - c.logger.Info(ctx, "context expired before sending node update") - return - case c.reqs <- &proto.CoordinateRequest{UpdateSelf: &proto.CoordinateRequest_UpdateSelf{Node: pn}}: - c.logger.Debug(ctx, "sent node in-memory to coordinator") - } - }) - return c -} - -func (c *inMemoryCoordination) respLoop() { - defer func() { - c.coordinatee.SetAllPeersLost() - close(c.respLoopDone) - }() - for resp := range c.resps { - c.logger.Debug(context.Background(), "got in-memory response from coordinator", slog.F("resp", resp)) - err := c.coordinatee.UpdatePeers(resp.GetPeerUpdates()) - if err != nil { - c.sendErr(xerrors.Errorf("failed to update peers: %w", err)) - return - } - } - c.logger.Debug(context.Background(), "in-memory response channel closed") -} - -func (*inMemoryCoordination) AwaitAck() <-chan struct{} { - // This is only used for tests, so just return a closed channel. - ch := make(chan struct{}) - close(ch) - return ch -} - -// Close attempts to gracefully close the remoteCoordination by sending a Disconnect message and -// waiting for the server to hang up the coordination. If the provided context expires, we stop -// waiting for the server and close the coordination stream from our end. -func (c *inMemoryCoordination) Close(ctx context.Context) error { - c.Lock() - defer c.Unlock() - c.logger.Debug(context.Background(), "closing in-memory coordination") - if c.closed { - return nil - } - defer close(c.reqs) - c.closed = true - select { - case <-ctx.Done(): - return xerrors.Errorf("failed to gracefully disconnect: %w", c.ctx.Err()) - case c.reqs <- &proto.CoordinateRequest{Disconnect: &proto.CoordinateRequest_Disconnect{}}: - c.logger.Debug(context.Background(), "sent graceful disconnect in-memory") - } - - select { - case <-ctx.Done(): - return xerrors.Errorf("context expired waiting for responses to close: %w", c.ctx.Err()) - case <-c.respLoopDone: - return nil - } -} - const LoggerName = "coord" var ( diff --git a/tailnet/coordinator_test.go b/tailnet/coordinator_test.go index b3a803cd6aaf6..67cf4768490f5 100644 --- a/tailnet/coordinator_test.go +++ b/tailnet/coordinator_test.go @@ -2,19 +2,11 @@ package tailnet_test import ( "context" - "io" - "net" "net/netip" - "sync" - "sync/atomic" "testing" - "time" "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - "tailscale.com/tailcfg" - "tailscale.com/types/key" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" @@ -271,265 +263,6 @@ func TestCoordinator_MultiAgent_CoordClose(t *testing.T) { ma1.RequireEventuallyClosed(ctx) } -func TestInMemoryCoordination(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - clientID := uuid.UUID{1} - agentID := uuid.UUID{2} - mCoord := tailnettest.NewMockCoordinator(gomock.NewController(t)) - fConn := &fakeCoordinatee{} - - reqs := make(chan *proto.CoordinateRequest, 100) - resps := make(chan *proto.CoordinateResponse, 100) - mCoord.EXPECT().Coordinate(gomock.Any(), clientID, gomock.Any(), tailnet.ClientCoordinateeAuth{agentID}). - Times(1).Return(reqs, resps) - - uut := tailnet.NewInMemoryCoordination(ctx, logger, clientID, agentID, mCoord, fConn) - defer uut.Close(ctx) - - coordinationTest(ctx, t, uut, fConn, reqs, resps, agentID) - - select { - case err := <-uut.Error(): - require.NoError(t, err) - default: - // OK! - } -} - -func TestRemoteCoordination(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - clientID := uuid.UUID{1} - agentID := uuid.UUID{2} - mCoord := tailnettest.NewMockCoordinator(gomock.NewController(t)) - fConn := &fakeCoordinatee{} - - reqs := make(chan *proto.CoordinateRequest, 100) - resps := make(chan *proto.CoordinateResponse, 100) - mCoord.EXPECT().Coordinate(gomock.Any(), clientID, gomock.Any(), tailnet.ClientCoordinateeAuth{agentID}). - Times(1).Return(reqs, resps) - - var coord tailnet.Coordinator = mCoord - coordPtr := atomic.Pointer[tailnet.Coordinator]{} - coordPtr.Store(&coord) - svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ - Logger: logger.Named("svc"), - CoordPtr: &coordPtr, - DERPMapUpdateFrequency: time.Hour, - DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, - NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, - ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), - }) - require.NoError(t, err) - sC, cC := net.Pipe() - - serveErr := make(chan error, 1) - go func() { - err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, tailnet.StreamID{ - Name: "client", - ID: clientID, - Auth: tailnet.ClientCoordinateeAuth{ - AgentID: agentID, - }, - }) - serveErr <- err - }() - - client, err := tailnet.NewDRPCClient(cC, logger) - require.NoError(t, err) - protocol, err := client.Coordinate(ctx) - require.NoError(t, err) - - uut := tailnet.NewRemoteCoordination(logger.Named("coordination"), protocol, fConn, agentID) - defer uut.Close(ctx) - - coordinationTest(ctx, t, uut, fConn, reqs, resps, agentID) - - // Recv loop should be terminated by the server hanging up after Disconnect - err = testutil.RequireRecvCtx(ctx, t, uut.Error()) - require.ErrorIs(t, err, io.EOF) -} - -func TestRemoteCoordination_SendsReadyForHandshake(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) - clientID := uuid.UUID{1} - agentID := uuid.UUID{2} - mCoord := tailnettest.NewMockCoordinator(gomock.NewController(t)) - fConn := &fakeCoordinatee{} - - reqs := make(chan *proto.CoordinateRequest, 100) - resps := make(chan *proto.CoordinateResponse, 100) - mCoord.EXPECT().Coordinate(gomock.Any(), clientID, gomock.Any(), tailnet.ClientCoordinateeAuth{agentID}). - Times(1).Return(reqs, resps) - - var coord tailnet.Coordinator = mCoord - coordPtr := atomic.Pointer[tailnet.Coordinator]{} - coordPtr.Store(&coord) - svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ - Logger: logger.Named("svc"), - CoordPtr: &coordPtr, - DERPMapUpdateFrequency: time.Hour, - DERPMapFn: func() *tailcfg.DERPMap { panic("not implemented") }, - NetworkTelemetryHandler: func(batch []*proto.TelemetryEvent) { panic("not implemented") }, - ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), - }) - require.NoError(t, err) - sC, cC := net.Pipe() - - serveErr := make(chan error, 1) - go func() { - err := svc.ServeClient(ctx, proto.CurrentVersion.String(), sC, tailnet.StreamID{ - Name: "client", - ID: clientID, - Auth: tailnet.ClientCoordinateeAuth{ - AgentID: agentID, - }, - }) - serveErr <- err - }() - - client, err := tailnet.NewDRPCClient(cC, logger) - require.NoError(t, err) - protocol, err := client.Coordinate(ctx) - require.NoError(t, err) - - uut := tailnet.NewRemoteCoordination(logger.Named("coordination"), protocol, fConn, uuid.UUID{}) - defer uut.Close(ctx) - - nk, err := key.NewNode().Public().MarshalBinary() - require.NoError(t, err) - dk, err := key.NewDisco().Public().MarshalText() - require.NoError(t, err) - testutil.RequireSendCtx(ctx, t, resps, &proto.CoordinateResponse{ - PeerUpdates: []*proto.CoordinateResponse_PeerUpdate{{ - Id: clientID[:], - Kind: proto.CoordinateResponse_PeerUpdate_NODE, - Node: &proto.Node{ - Id: 3, - Key: nk, - Disco: string(dk), - }, - }}, - }) - - rfh := testutil.RequireRecvCtx(ctx, t, reqs) - require.NotNil(t, rfh.ReadyForHandshake) - require.Len(t, rfh.ReadyForHandshake, 1) - require.Equal(t, clientID[:], rfh.ReadyForHandshake[0].Id) - - go uut.Close(ctx) - dis := testutil.RequireRecvCtx(ctx, t, reqs) - require.NotNil(t, dis) - require.NotNil(t, dis.Disconnect) - close(resps) - - // Recv loop should be terminated by the server hanging up after Disconnect - err = testutil.RequireRecvCtx(ctx, t, uut.Error()) - require.ErrorIs(t, err, io.EOF) -} - -// coordinationTest tests that a coordination behaves correctly -func coordinationTest( - ctx context.Context, t *testing.T, - uut tailnet.Coordination, fConn *fakeCoordinatee, - reqs chan *proto.CoordinateRequest, resps chan *proto.CoordinateResponse, - agentID uuid.UUID, -) { - // It should add the tunnel, since we configured as a client - req := testutil.RequireRecvCtx(ctx, t, reqs) - require.Equal(t, agentID[:], req.GetAddTunnel().GetId()) - - // when we call the callback, it should send a node update - require.NotNil(t, fConn.callback) - fConn.callback(&tailnet.Node{PreferredDERP: 1}) - - req = testutil.RequireRecvCtx(ctx, t, reqs) - require.Equal(t, int32(1), req.GetUpdateSelf().GetNode().GetPreferredDerp()) - - // When we send a peer update, it should update the coordinatee - nk, err := key.NewNode().Public().MarshalBinary() - require.NoError(t, err) - dk, err := key.NewDisco().Public().MarshalText() - require.NoError(t, err) - updates := []*proto.CoordinateResponse_PeerUpdate{ - { - Id: agentID[:], - Kind: proto.CoordinateResponse_PeerUpdate_NODE, - Node: &proto.Node{ - Id: 2, - Key: nk, - Disco: string(dk), - }, - }, - } - testutil.RequireSendCtx(ctx, t, resps, &proto.CoordinateResponse{PeerUpdates: updates}) - require.Eventually(t, func() bool { - fConn.Lock() - defer fConn.Unlock() - return len(fConn.updates) > 0 - }, testutil.WaitShort, testutil.IntervalFast) - require.Len(t, fConn.updates[0], 1) - require.Equal(t, agentID[:], fConn.updates[0][0].Id) - - errCh := make(chan error, 1) - go func() { - errCh <- uut.Close(ctx) - }() - - // When we close, it should gracefully disconnect - req = testutil.RequireRecvCtx(ctx, t, reqs) - require.NotNil(t, req.Disconnect) - close(resps) - - err = testutil.RequireRecvCtx(ctx, t, errCh) - require.NoError(t, err) - - // It should set all peers lost on the coordinatee - require.Equal(t, 1, fConn.setAllPeersLostCalls) -} - -type fakeCoordinatee struct { - sync.Mutex - callback func(*tailnet.Node) - updates [][]*proto.CoordinateResponse_PeerUpdate - setAllPeersLostCalls int - tunnelDestinations map[uuid.UUID]struct{} -} - -func (f *fakeCoordinatee) UpdatePeers(updates []*proto.CoordinateResponse_PeerUpdate) error { - f.Lock() - defer f.Unlock() - f.updates = append(f.updates, updates) - return nil -} - -func (f *fakeCoordinatee) SetAllPeersLost() { - f.Lock() - defer f.Unlock() - f.setAllPeersLostCalls++ -} - -func (f *fakeCoordinatee) SetTunnelDestination(id uuid.UUID) { - f.Lock() - defer f.Unlock() - - if f.tunnelDestinations == nil { - f.tunnelDestinations = map[uuid.UUID]struct{}{} - } - f.tunnelDestinations[id] = struct{}{} -} - -func (f *fakeCoordinatee) SetNodeCallback(callback func(*tailnet.Node)) { - f.Lock() - defer f.Unlock() - f.callback = callback -} - // TestCoordinatorPropogatedPeerContext tests that the context for a specific peer // is propogated through to the `Authorize“ method of the coordinatee auth func TestCoordinatorPropogatedPeerContext(t *testing.T) { diff --git a/tailnet/test/integration/integration.go b/tailnet/test/integration/integration.go index ff38aec98b394..232e7ab027d72 100644 --- a/tailnet/test/integration/integration.go +++ b/tailnet/test/integration/integration.go @@ -467,7 +467,8 @@ func startClientOptions(t *testing.T, logger slog.Logger, serverURL *url.URL, me _ = conn.Close() }) - coordination := tailnet.NewRemoteCoordination(logger, coord, conn, peer.ID) + ctrl := tailnet.NewSingleDestController(logger, conn, peer.ID) + coordination := ctrl.New(coord) t.Cleanup(func() { cctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() From 2d00b50eb6d4e5e245b26f848ceb1d6bf02dcdd0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 5 Nov 2024 08:12:56 -0600 Subject: [PATCH 064/223] chore: remove excess join in GetQuotaConsumedForUser query (#15338) Filter is applied in original workspace query. We do not need to join `workspaces` twice. Use build_number instead of `created_at` for determining the last build. --- coderd/database/dbgen/dbgen.go | 9 +++ coderd/database/queries.sql.go | 16 ++-- coderd/database/queries/quotas.sql | 16 ++-- enterprise/coderd/workspacequota_test.go | 98 +++++++++++++++++++++++- 4 files changed, 115 insertions(+), 24 deletions(-) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 4ac675309f662..5e83125a93b84 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -288,6 +288,15 @@ func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuil if err != nil { return err } + + if orig.DailyCost > 0 { + err = db.UpdateWorkspaceBuildCostByID(genCtx, database.UpdateWorkspaceBuildCostByIDParams{ + ID: buildID, + DailyCost: orig.DailyCost, + }) + require.NoError(t, err) + } + build, err = db.GetWorkspaceBuildByID(genCtx, buildID) if err != nil { return err diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 7d3e97166aa8c..87d3c17f5400f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6746,25 +6746,19 @@ FROM INNER JOIN workspaces on wb.workspace_id = workspaces.id WHERE + -- Only return workspaces that match the user + organization. + -- Quotas are calculated per user per organization. + NOT workspaces.deleted AND workspaces.owner_id = $1 AND workspaces.organization_id = $2 ORDER BY wb.workspace_id, - wb.created_at DESC + wb.build_number DESC ) SELECT coalesce(SUM(daily_cost), 0)::BIGINT FROM - workspaces -INNER JOIN latest_builds ON - latest_builds.workspace_id = workspaces.id -WHERE - NOT deleted AND - -- We can likely remove these conditions since we check above. - -- But it does not hurt to be defensive and make sure future query changes - -- do not break anything. - workspaces.owner_id = $1 AND - workspaces.organization_id = $2 + latest_builds ` type GetQuotaConsumedForUserParams struct { diff --git a/coderd/database/queries/quotas.sql b/coderd/database/queries/quotas.sql index 7ab6189dfe8a1..5190057fe68bc 100644 --- a/coderd/database/queries/quotas.sql +++ b/coderd/database/queries/quotas.sql @@ -28,23 +28,17 @@ FROM INNER JOIN workspaces on wb.workspace_id = workspaces.id WHERE + -- Only return workspaces that match the user + organization. + -- Quotas are calculated per user per organization. + NOT workspaces.deleted AND workspaces.owner_id = @owner_id AND workspaces.organization_id = @organization_id ORDER BY wb.workspace_id, - wb.created_at DESC + wb.build_number DESC ) SELECT coalesce(SUM(daily_cost), 0)::BIGINT FROM - workspaces -INNER JOIN latest_builds ON - latest_builds.workspace_id = workspaces.id -WHERE - NOT deleted AND - -- We can likely remove these conditions since we check above. - -- But it does not hurt to be defensive and make sure future query changes - -- do not break anything. - workspaces.owner_id = @owner_id AND - workspaces.organization_id = @organization_id + latest_builds ; diff --git a/enterprise/coderd/workspacequota_test.go b/enterprise/coderd/workspacequota_test.go index 13142f11e5717..5ec308eb6de62 100644 --- a/enterprise/coderd/workspacequota_test.go +++ b/enterprise/coderd/workspacequota_test.go @@ -21,6 +21,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" @@ -31,9 +32,13 @@ import ( ) func verifyQuota(ctx context.Context, t *testing.T, client *codersdk.Client, organizationID string, consumed, total int) { + verifyQuotaUser(ctx, t, client, organizationID, codersdk.Me, consumed, total) +} + +func verifyQuotaUser(ctx context.Context, t *testing.T, client *codersdk.Client, organizationID string, user string, consumed, total int) { t.Helper() - got, err := client.WorkspaceQuota(ctx, organizationID, codersdk.Me) + got, err := client.WorkspaceQuota(ctx, organizationID, user) require.NoError(t, err) require.EqualValues(t, codersdk.WorkspaceQuota{ Budget: total, @@ -43,7 +48,7 @@ func verifyQuota(ctx context.Context, t *testing.T, client *codersdk.Client, org // Remove this check when the deprecated endpoint is removed. // This just makes sure the deprecated endpoint is still working // as intended. It will only work for the default organization. - deprecatedGot, err := deprecatedQuotaEndpoint(ctx, client, codersdk.Me) + deprecatedGot, err := deprecatedQuotaEndpoint(ctx, client, user) require.NoError(t, err, "deprecated endpoint") // Only continue to check if the values differ if deprecatedGot.Budget != got.Budget || deprecatedGot.CreditsConsumed != got.CreditsConsumed { @@ -300,6 +305,95 @@ func TestWorkspaceQuota(t *testing.T) { verifyQuota(ctx, t, owner, first.OrganizationID.String(), 0, 30) verifyQuota(ctx, t, owner, second.ID.String(), 0, 15) }) + + // ManyWorkspaces uses dbfake and dbgen to insert a scenario into the db. + t.Run("ManyWorkspaces", func(t *testing.T) { + t.Parallel() + + owner, db, first := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleOwner()) + + // Prepopulate database. Use dbfake as it is quicker and + // easier than the api. + ctx := testutil.Context(t, testutil.WaitLong) + + user := dbgen.User(t, db, database.User{}) + noise := dbgen.User(t, db, database.User{}) + + second := dbfake.Organization(t, db). + Members(user, noise). + EveryoneAllowance(10). + Group(database.Group{ + QuotaAllowance: 25, + }, user, noise). + Group(database.Group{ + QuotaAllowance: 30, + }, noise). + Do() + + third := dbfake.Organization(t, db). + Members(noise). + EveryoneAllowance(7). + Do() + + verifyQuotaUser(ctx, t, client, second.Org.ID.String(), user.ID.String(), 0, 35) + verifyQuotaUser(ctx, t, client, second.Org.ID.String(), noise.ID.String(), 0, 65) + + // Workspaces owned by the user + consumed := 0 + for i := 0; i < 2; i++ { + const cost = 5 + dbfake.WorkspaceBuild(t, db, + database.WorkspaceTable{ + OwnerID: user.ID, + OrganizationID: second.Org.ID, + }). + Seed(database.WorkspaceBuild{ + DailyCost: cost, + }).Do() + consumed += cost + } + + // Add some noise + // Workspace by the user in the third org + dbfake.WorkspaceBuild(t, db, + database.WorkspaceTable{ + OwnerID: user.ID, + OrganizationID: third.Org.ID, + }). + Seed(database.WorkspaceBuild{ + DailyCost: 10, + }).Do() + + // Workspace by another user in third org + dbfake.WorkspaceBuild(t, db, + database.WorkspaceTable{ + OwnerID: noise.ID, + OrganizationID: third.Org.ID, + }). + Seed(database.WorkspaceBuild{ + DailyCost: 10, + }).Do() + + // Workspace by another user in second org + dbfake.WorkspaceBuild(t, db, + database.WorkspaceTable{ + OwnerID: noise.ID, + OrganizationID: second.Org.ID, + }). + Seed(database.WorkspaceBuild{ + DailyCost: 10, + }).Do() + + verifyQuotaUser(ctx, t, client, second.Org.ID.String(), user.ID.String(), consumed, 35) + }) } // nolint:paralleltest,tparallel // Tests must run serially From 3c60dc3bb5183db8f05a13801e15b1769ddea9b5 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:18:38 +1100 Subject: [PATCH 065/223] fix(site): show error on duplicate template rename attempt (#15348) Fixes #15311. --- coderd/templates.go | 12 +++++++- coderd/templates_test.go | 27 ++++++++++++++++++ .../TemplateSettingsPage.test.tsx | 28 ++++++++++++++++++- .../TemplateSettingsPage.tsx | 6 +++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/coderd/templates.go b/coderd/templates.go index 82f805f5a09c0..76534e2328f92 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -841,7 +841,17 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { return nil }, nil) if err != nil { - httpapi.InternalServerError(rw, err) + if database.IsUniqueViolation(err, database.UniqueTemplatesOrganizationIDNameIndex) { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: fmt.Sprintf("Template with name %q already exists.", req.Name), + Validations: []codersdk.ValidationError{{ + Field: "name", + Detail: "This value is already in use and should be unique.", + }}, + }) + } else { + httpapi.InternalServerError(rw, err) + } return } diff --git a/coderd/templates_test.go b/coderd/templates_test.go index c1f1f8f1bbed2..3368aa582daf1 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" @@ -612,6 +613,32 @@ func TestPatchTemplateMeta(t *testing.T) { assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action) }) + t.Run("AlreadyExists", func(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires Postgres constraints") + } + + ownerClient := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, ownerClient) + client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + template2 := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template2.Name, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + t.Run("AGPL_Deprecated", func(t *testing.T) { t.Parallel() diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 7da1de5ccecab..cfe52db26a5a8 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -4,7 +4,11 @@ import { API, withDefaultFeatures } from "api/api"; import type { Template, UpdateTemplateMeta } from "api/typesGenerated"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { http, HttpResponse } from "msw"; -import { MockEntitlements, MockTemplate } from "testHelpers/entities"; +import { + MockEntitlements, + MockTemplate, + mockApiError, +} from "testHelpers/entities"; import { renderWithTemplateSettingsLayout, waitForLoaderToBeRemoved, @@ -112,6 +116,28 @@ describe("TemplateSettingsPage", () => { await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); }); + it("displays an error if the name is taken", async () => { + await renderTemplateSettingsPage(); + jest.spyOn(API, "updateTemplateMeta").mockRejectedValueOnce( + mockApiError({ + message: `Template with name "test-template" already exists`, + validations: [ + { + field: "name", + detail: "This value is already in use and should be unique.", + }, + ], + }), + ); + await fillAndSubmitForm(validFormValues); + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); + expect( + await screen.findByText( + "This value is already in use and should be unique.", + ), + ).toBeInTheDocument(); + }); + it("allows a description of 128 chars", () => { const values: UpdateTemplateMeta = { ...validFormValues, diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index e0bd36f7f77a1..9b04282d38ab4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -1,7 +1,8 @@ import { API } from "api/api"; +import { getErrorMessage } from "api/errors"; import { templateByNameKey } from "api/queries/templates"; import type { UpdateTemplateMeta } from "api/typesGenerated"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { useDashboard } from "modules/dashboard/useDashboard"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; @@ -51,6 +52,9 @@ export const TemplateSettingsPage: FC = () => { displaySuccess("Template updated successfully"); navigate(getLink(linkToTemplate(data.organization_name, data.name))); }, + onError: (error) => { + displayError(getErrorMessage(error, "Failed to update template")); + }, }, ); From 8b5a18cadee6184e29aa8f67216468eff137cdc4 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:20:27 +1100 Subject: [PATCH 066/223] fix(site): watch build logs while job is pending or running (#15341) Closes #15292. Currently, if the frontend never sees a build job enter 'running', it'll never end up watching the logs. If we start watching the logs earlier we're able to catch cases where the job goes `pending` -> `failed`, such as when the build fails immediately. --- site/src/modules/templates/useWatchVersionLogs.ts | 5 ++++- .../TemplateVersionEditorPage.test.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/site/src/modules/templates/useWatchVersionLogs.ts b/site/src/modules/templates/useWatchVersionLogs.ts index 586499fb0c944..5574e083a9849 100644 --- a/site/src/modules/templates/useWatchVersionLogs.ts +++ b/site/src/modules/templates/useWatchVersionLogs.ts @@ -20,7 +20,10 @@ export const useWatchVersionLogs = ( return; } - if (templateVersionStatus !== "running") { + if ( + templateVersionStatus !== "running" && + templateVersionStatus !== "pending" + ) { return; } diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 916184c2d4bbc..2cb97396a2f60 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -324,14 +324,13 @@ test("display pending badge and update it to running when status changes", async }, }; - let calls = 0; + let running = false; server.use( http.get( "/api/v2/organizations/:org/templates/:template/versions/:version", () => { - calls += 1; return HttpResponse.json( - calls > 1 ? MockRunningTemplateVersion : MockPendingTemplateVersion, + running ? MockRunningTemplateVersion : MockPendingTemplateVersion, ); }, ), @@ -348,6 +347,10 @@ test("display pending badge and update it to running when status changes", async const status = await screen.findByRole("status"); expect(status).toHaveTextContent("Pending"); + // Manually update the endpoint, as to not rely on the editor page + // making a specific number of requests. + running = true; + await waitFor( () => { expect(status).toHaveTextContent("Running"); From 4fe2c5f09ae3cc393a4b38b097638c1bdbd7a4b8 Mon Sep 17 00:00:00 2001 From: Vincent Vielle Date: Tue, 5 Nov 2024 17:22:32 +0100 Subject: [PATCH 067/223] fix: improve password validation flow (#15132) Refers to #14984 Currently, password validation is done backend side and is not explicit enough so it can be painful to create first users. We'd like to make this validation easier - but also duplicate it frontend side to make it smoother. Flows involved : - First user set password - New user set password - Change password --------- Co-authored-by: BrunoQuaresma --- coderd/apidoc/docs.go | 61 ++++++++ coderd/apidoc/swagger.json | 53 +++++++ coderd/coderd.go | 1 + coderd/userauth.go | 35 +++++ coderd/userpassword/userpassword_test.go | 138 ++++++++++++------ coderd/users_test.go | 18 +++ codersdk/users.go | 23 +++ docs/reference/api/authorization.md | 47 ++++++ docs/reference/api/schemas.md | 30 ++++ site/src/api/api.ts | 9 ++ site/src/api/queries/users.ts | 1 + site/src/api/typesGenerated.ts | 11 ++ .../PasswordField/PasswordField.stories.tsx | 66 +++++++++ .../PasswordField/PasswordField.tsx | 37 +++++ .../pages/CreateUserPage/CreateUserForm.tsx | 6 +- .../pages/CreateUserPage/CreateUserPage.tsx | 3 +- site/src/pages/SetupPage/SetupPage.test.tsx | 27 ++++ site/src/pages/SetupPage/SetupPage.tsx | 3 +- site/src/pages/SetupPage/SetupPageView.tsx | 7 +- .../SecurityPage/SecurityForm.tsx | 12 +- .../SecurityPage/SecurityPage.test.tsx | 18 --- 21 files changed, 531 insertions(+), 75 deletions(-) create mode 100644 site/src/components/PasswordField/PasswordField.stories.tsx create mode 100644 site/src/components/PasswordField/PasswordField.tsx diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a8719397a1559..6c770c18232ac 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5373,6 +5373,45 @@ const docTemplate = `{ } } }, + "/users/validate-password": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Validate user password", + "operationId": "validate-user-password", + "parameters": [ + { + "description": "Validate user password request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.ValidateUserPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ValidateUserPasswordResponse" + } + } + } + } + }, "/users/{user}": { "get": { "security": [ @@ -14096,6 +14135,28 @@ const docTemplate = `{ "UserStatusSuspended" ] }, + "codersdk.ValidateUserPasswordRequest": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "password": { + "type": "string" + } + } + }, + "codersdk.ValidateUserPasswordResponse": { + "type": "object", + "properties": { + "details": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, "codersdk.ValidationError": { "type": "object", "required": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 88bf71bf05758..4f5ca444f703e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4737,6 +4737,39 @@ } } }, + "/users/validate-password": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Authorization"], + "summary": "Validate user password", + "operationId": "validate-user-password", + "parameters": [ + { + "description": "Validate user password request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.ValidateUserPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ValidateUserPasswordResponse" + } + } + } + } + }, "/users/{user}": { "get": { "security": [ @@ -12817,6 +12850,26 @@ "UserStatusSuspended" ] }, + "codersdk.ValidateUserPasswordRequest": { + "type": "object", + "required": ["password"], + "properties": { + "password": { + "type": "string" + } + } + }, + "codersdk.ValidateUserPasswordResponse": { + "type": "object", + "properties": { + "details": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, "codersdk.ValidationError": { "type": "object", "required": ["detail", "field"], diff --git a/coderd/coderd.go b/coderd/coderd.go index 39df674fecca8..c3a780b2b1106 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1047,6 +1047,7 @@ func New(options *Options) *API { r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute)) r.Post("/login", api.postLogin) r.Post("/otp/request", api.postRequestOneTimePasscode) + r.Post("/validate-password", api.validateUserPassword) r.Post("/otp/change-password", api.postChangePasswordWithOneTimePasscode) r.Route("/oauth2", func(r chi.Router) { r.Route("/github", func(r chi.Router) { diff --git a/coderd/userauth.go b/coderd/userauth.go index 317bb5b6a9e58..f6cf0e5292db7 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -447,6 +447,41 @@ func (api *API) postChangePasswordWithOneTimePasscode(rw http.ResponseWriter, r } } +// ValidateUserPassword validates the complexity of a user password and that it is secured enough. +// +// @Summary Validate user password +// @ID validate-user-password +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Authorization +// @Param request body codersdk.ValidateUserPasswordRequest true "Validate user password request" +// @Success 200 {object} codersdk.ValidateUserPasswordResponse +// @Router /users/validate-password [post] +func (*API) validateUserPassword(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + valid = true + details = "" + ) + + var req codersdk.ValidateUserPasswordRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + err := userpassword.Validate(req.Password) + if err != nil { + valid = false + details = err.Error() + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.ValidateUserPasswordResponse{ + Valid: valid, + Details: details, + }) +} + // Authenticates the user with an email and password. // // @Summary Log in user diff --git a/coderd/userpassword/userpassword_test.go b/coderd/userpassword/userpassword_test.go index 1617748d5ada1..41eebf49c974d 100644 --- a/coderd/userpassword/userpassword_test.go +++ b/coderd/userpassword/userpassword_test.go @@ -5,6 +5,7 @@ package userpassword_test import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -12,46 +13,101 @@ import ( "github.com/coder/coder/v2/coderd/userpassword" ) -func TestUserPassword(t *testing.T) { +func TestUserPasswordValidate(t *testing.T) { t.Parallel() - t.Run("Legacy", func(t *testing.T) { - t.Parallel() - // Ensures legacy v1 passwords function for v2. - // This has is manually generated using a print statement from v1 code. - equal, err := userpassword.Compare("$pbkdf2-sha256$65535$z8c1p1C2ru9EImBP1I+ZNA$pNjE3Yk0oG0PmJ0Je+y7ENOVlSkn/b0BEqqdKsq6Y97wQBq0xT+lD5bWJpyIKJqQICuPZcEaGDKrXJn8+SIHRg", "tomato") - require.NoError(t, err) - require.True(t, equal) - }) - - t.Run("Same", func(t *testing.T) { - t.Parallel() - hash, err := userpassword.Hash("password") - require.NoError(t, err) - equal, err := userpassword.Compare(hash, "password") - require.NoError(t, err) - require.True(t, equal) - }) - - t.Run("Different", func(t *testing.T) { - t.Parallel() - hash, err := userpassword.Hash("password") - require.NoError(t, err) - equal, err := userpassword.Compare(hash, "notpassword") - require.NoError(t, err) - require.False(t, equal) - }) - - t.Run("Invalid", func(t *testing.T) { - t.Parallel() - equal, err := userpassword.Compare("invalidhash", "password") - require.False(t, equal) - require.Error(t, err) - }) - - t.Run("InvalidParts", func(t *testing.T) { - t.Parallel() - equal, err := userpassword.Compare("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "test") - require.False(t, equal) - require.Error(t, err) - }) + tests := []struct { + name string + password string + wantErr bool + }{ + {name: "Invalid - Too short password", password: "pass", wantErr: true}, + {name: "Invalid - Too long password", password: strings.Repeat("a", 65), wantErr: true}, + {name: "Invalid - easy password", password: "password", wantErr: true}, + {name: "Ok", password: "PasswordSecured123!", wantErr: false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := userpassword.Validate(tt.password) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestUserPasswordCompare(t *testing.T) { + t.Parallel() + tests := []struct { + name string + passwordToValidate string + password string + shouldHash bool + wantErr bool + wantEqual bool + }{ + { + name: "Legacy", + passwordToValidate: "$pbkdf2-sha256$65535$z8c1p1C2ru9EImBP1I+ZNA$pNjE3Yk0oG0PmJ0Je+y7ENOVlSkn/b0BEqqdKsq6Y97wQBq0xT+lD5bWJpyIKJqQICuPZcEaGDKrXJn8+SIHRg", + password: "tomato", + shouldHash: false, + wantErr: false, + wantEqual: true, + }, + { + name: "Same", + passwordToValidate: "password", + password: "password", + shouldHash: true, + wantErr: false, + wantEqual: true, + }, + { + name: "Different", + passwordToValidate: "password", + password: "notpassword", + shouldHash: true, + wantErr: false, + wantEqual: false, + }, + { + name: "Invalid", + passwordToValidate: "invalidhash", + password: "password", + shouldHash: false, + wantErr: true, + wantEqual: false, + }, + { + name: "InvalidParts", + passwordToValidate: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", + password: "test", + shouldHash: false, + wantErr: true, + wantEqual: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if tt.shouldHash { + hash, err := userpassword.Hash(tt.passwordToValidate) + require.NoError(t, err) + tt.passwordToValidate = hash + } + equal, err := userpassword.Compare(tt.passwordToValidate, tt.password) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.wantEqual, equal) + }) + } } diff --git a/coderd/users_test.go b/coderd/users_test.go index 3c88d3e5022ac..375e4b3168066 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1219,6 +1219,24 @@ func TestUpdateUserPassword(t *testing.T) { require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action) }) + t.Run("ValidateUserPassword", func(t *testing.T) { + t.Parallel() + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor}) + + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + resp, err := client.ValidateUserPassword(ctx, codersdk.ValidateUserPasswordRequest{ + Password: "MySecurePassword!", + }) + + require.NoError(t, err, "users shoud be able to validate complexity of a potential new password") + require.True(t, resp.Valid) + }) + t.Run("ChangingPasswordDeletesKeys", func(t *testing.T) { t.Parallel() diff --git a/codersdk/users.go b/codersdk/users.go index 546fcc99e9fbe..4dbdc0d4e4f91 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -178,6 +178,15 @@ type UpdateUserProfileRequest struct { Name string `json:"name" validate:"user_real_name"` } +type ValidateUserPasswordRequest struct { + Password string `json:"password" validate:"required"` +} + +type ValidateUserPasswordResponse struct { + Valid bool `json:"valid"` + Details string `json:"details"` +} + type UpdateUserAppearanceSettingsRequest struct { ThemePreference string `json:"theme_preference" validate:"required"` } @@ -407,6 +416,20 @@ func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateU return resp, json.NewDecoder(res.Body).Decode(&resp) } +// ValidateUserPassword validates the complexity of a user password and that it is secured enough. +func (c *Client) ValidateUserPassword(ctx context.Context, req ValidateUserPasswordRequest) (ValidateUserPasswordResponse, error) { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/validate-password", req) + if err != nil { + return ValidateUserPasswordResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ValidateUserPasswordResponse{}, ReadBodyAsError(res) + } + var resp ValidateUserPasswordResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + // UpdateUserStatus sets the user status to the given status func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserStatus) (User, error) { path := fmt.Sprintf("/api/v2/users/%s/status/", user) diff --git a/docs/reference/api/authorization.md b/docs/reference/api/authorization.md index 86cee5d0fd727..9dfbfb620870f 100644 --- a/docs/reference/api/authorization.md +++ b/docs/reference/api/authorization.md @@ -178,6 +178,53 @@ curl -X POST http://coder-server:8080/api/v2/users/otp/request \ | ------ | --------------------------------------------------------------- | ----------- | ------ | | 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | | +## Validate user password + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/users/validate-password \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /users/validate-password` + +> Body parameter + +```json +{ + "password": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | -------------------------------------------------------------------------------------- | -------- | ------------------------------ | +| `body` | body | [codersdk.ValidateUserPasswordRequest](schemas.md#codersdkvalidateuserpasswordrequest) | true | Validate user password request | + +### Example responses + +> 200 Response + +```json +{ + "details": "string", + "valid": true +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ValidateUserPasswordResponse](schemas.md#codersdkvalidateuserpasswordresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Convert user from password to oauth authentication ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index c7c1a729476c8..dab7703345b08 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6446,6 +6446,36 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `dormant` | | `suspended` | +## codersdk.ValidateUserPasswordRequest + +```json +{ + "password": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------- | ------ | -------- | ------------ | ----------- | +| `password` | string | true | | | + +## codersdk.ValidateUserPasswordResponse + +```json +{ + "details": "string", + "valid": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------- | ------- | -------- | ------------ | ----------- | +| `details` | string | false | | | +| `valid` | boolean | false | | | + ## codersdk.ValidationError ```json diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b79fea12a0c31..e9d5e3fbf3f15 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1322,6 +1322,15 @@ class ApiMethods { await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); }; + validateUserPassword = async ( + password: string, + ): Promise => { + const response = await this.axios.post("/api/v2/users/validate-password", { + password, + }); + return response.data; + }; + getRoles = async (): Promise> => { const response = await this.axios.get( "/api/v2/users/roles", diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 833d88e6baeef..77d879abe3258 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -9,6 +9,7 @@ import type { UpdateUserProfileRequest, User, UsersRequest, + ValidateUserPasswordRequest, } from "api/typesGenerated"; import { type MetadataState, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 619961c457b36..f89d9eada822c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1779,6 +1779,17 @@ export interface UsersRequest extends Pagination { readonly q?: string; } +// From codersdk/users.go +export interface ValidateUserPasswordRequest { + readonly password: string; +} + +// From codersdk/users.go +export interface ValidateUserPasswordResponse { + readonly valid: boolean; + readonly details: string; +} + // From codersdk/client.go export interface ValidationError { readonly field: string; diff --git a/site/src/components/PasswordField/PasswordField.stories.tsx b/site/src/components/PasswordField/PasswordField.stories.tsx new file mode 100644 index 0000000000000..4eba909c4c6ef --- /dev/null +++ b/site/src/components/PasswordField/PasswordField.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, spyOn, userEvent, waitFor, within } from "@storybook/test"; +import { API } from "api/api"; +import { useState } from "react"; +import { PasswordField } from "./PasswordField"; + +const meta: Meta = { + title: "components/PasswordField", + component: PasswordField, + args: { + label: "Password", + }, + render: function StatefulPasswordField(args) { + const [value, setValue] = useState(""); + return ( + setValue(e.currentTarget.value)} + /> + ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Idle: Story = {}; + +const securePassword = "s3curePa$$w0rd"; +export const Valid: Story = { + play: async ({ canvasElement }) => { + const validatePasswordSpy = spyOn( + API, + "validateUserPassword", + ).mockResolvedValueOnce({ valid: true, details: "" }); + const user = userEvent.setup(); + const canvas = within(canvasElement); + const input = canvas.getByLabelText("Password"); + await user.type(input, securePassword); + await waitFor(() => + expect(validatePasswordSpy).toHaveBeenCalledWith(securePassword), + ); + expect(validatePasswordSpy).toHaveBeenCalledTimes(1); + }, +}; + +export const Invalid: Story = { + play: async ({ canvasElement }) => { + const validatePasswordSpy = spyOn( + API, + "validateUserPassword", + ).mockResolvedValueOnce({ + valid: false, + details: "Password is too short.", + }); + const user = userEvent.setup(); + const canvas = within(canvasElement); + const input = canvas.getByLabelText("Password"); + await user.type(input, securePassword); + await waitFor(() => + expect(validatePasswordSpy).toHaveBeenCalledWith(securePassword), + ); + expect(validatePasswordSpy).toHaveBeenCalledTimes(1); + }, +}; diff --git a/site/src/components/PasswordField/PasswordField.tsx b/site/src/components/PasswordField/PasswordField.tsx new file mode 100644 index 0000000000000..276526bb84632 --- /dev/null +++ b/site/src/components/PasswordField/PasswordField.tsx @@ -0,0 +1,37 @@ +import TextField, { type TextFieldProps } from "@mui/material/TextField"; +import { API } from "api/api"; +import { useDebouncedValue } from "hooks/debounce"; +import type { FC } from "react"; +import { useQuery } from "react-query"; + +// TODO: @BrunoQuaresma: Unable to integrate Yup + Formik for validation. The +// validation was triggering on the onChange event, but the form.errors were not +// updating accordingly. Tried various combinations of validateOnBlur and +// validateOnChange without success. Further investigation is needed. + +/** + * A password field component that validates the password against the API with + * debounced calls. It uses a debounced value to minimize the number of API + * calls and displays validation errors. + */ +export const PasswordField: FC = (props) => { + const debouncedValue = useDebouncedValue(`${props.value}`, 500); + const validatePasswordQuery = useQuery({ + queryKey: ["validatePassword", debouncedValue], + queryFn: () => API.validateUserPassword(debouncedValue), + keepPreviousData: true, + enabled: debouncedValue.length > 0, + }); + const valid = validatePasswordQuery.data?.valid ?? true; + + return ( + + ); +}; diff --git a/site/src/pages/CreateUserPage/CreateUserForm.tsx b/site/src/pages/CreateUserPage/CreateUserForm.tsx index 635c26387c00c..51dae50df26fa 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.tsx @@ -6,9 +6,10 @@ import type * as TypesGen from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { FormFooter } from "components/FormFooter/FormFooter"; import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { PasswordField } from "components/PasswordField/PasswordField"; import { Stack } from "components/Stack/Stack"; import { type FormikContextType, useFormik } from "formik"; -import type { FC } from "react"; +import { type FC, useEffect } from "react"; import { displayNameValidator, getFormHelpers, @@ -186,7 +187,7 @@ export const CreateUserForm: FC< ); })} - { ); }); + it("renders the password validation error", async () => { + server.use( + http.post("/api/v2/users/validate-password", () => { + return HttpResponse.json({ + valid: false, + details: "Password is too short", + }); + }), + ); + + renderWithRouter( + createMemoryRouter( + [ + { + path: "/setup", + element: , + }, + ], + { initialEntries: ["/setup"] }, + ), + ); + await waitForLoaderToBeRemoved(); + await fillForm({ password: "short" }); + await waitFor(() => screen.findByText("Password is too short")); + }); + it("redirects to the app when setup is successful", async () => { let userHasBeenCreated = false; @@ -99,6 +125,7 @@ describe("Setup Page", () => { await fillForm(); await waitFor(() => screen.findByText("Templates")); }); + it("calls sendBeacon with telemetry", async () => { const sendBeacon = jest.fn(); Object.defineProperty(window.navigator, "sendBeacon", { diff --git a/site/src/pages/SetupPage/SetupPage.tsx b/site/src/pages/SetupPage/SetupPage.tsx index ed07534919481..100c02e21334e 100644 --- a/site/src/pages/SetupPage/SetupPage.tsx +++ b/site/src/pages/SetupPage/SetupPage.tsx @@ -3,7 +3,7 @@ import { createFirstUser } from "api/queries/users"; import { Loader } from "components/Loader/Loader"; import { useAuthContext } from "contexts/auth/AuthProvider"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { type FC, useEffect } from "react"; +import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; import { Navigate, useNavigate } from "react-router-dom"; @@ -24,6 +24,7 @@ export const SetupPage: FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); const navigate = useNavigate(); + useEffect(() => { if (!buildInfoQuery.data) { return; diff --git a/site/src/pages/SetupPage/SetupPageView.tsx b/site/src/pages/SetupPage/SetupPageView.tsx index a4b0536ae0b85..d76a55924aa63 100644 --- a/site/src/pages/SetupPage/SetupPageView.tsx +++ b/site/src/pages/SetupPage/SetupPageView.tsx @@ -10,10 +10,12 @@ import { isAxiosError } from "axios"; import { Alert, AlertDetail } from "components/Alert/Alert"; import { FormFields, VerticalForm } from "components/Form/Form"; import { CoderIcon } from "components/Icons/CoderIcon"; +import { PasswordField } from "components/PasswordField/PasswordField"; import { SignInLayout } from "components/SignInLayout/SignInLayout"; import { Stack } from "components/Stack/Stack"; import { type FormikContextType, useFormik } from "formik"; import type { FC } from "react"; +import { useEffect } from "react"; import { docs } from "utils/docs"; import { getFormHelpers, @@ -33,7 +35,6 @@ export const Language = { passwordRequired: "Please enter a password.", create: "Create account", welcomeMessage: <>Welcome to Coder, - firstNameLabel: "First name", lastNameLabel: "Last name", companyLabel: "Company", @@ -167,13 +168,11 @@ export const SetupPageView: FC = ({ fullWidth label={Language.emailLabel} /> -
Release notes

Sourced from eslint-config-next's releases.

v14.2.15

[!NOTE]
This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

  • support breadcrumb style catch-all parallel routes #65063
  • Provide non-dynamic segments to catch-all parallel routes #65233
  • Fix client reference access causing metadata missing #70732
  • feat(next/image): add support for decoding prop #70298
  • feat(next/image): add images.localPatterns config #70529
  • fix(next/image): handle undefined images.localPatterns config in images-manifest.json
  • fix: Do not omit alt on getImgProps return type, ImgProps #70608
  • [i18n] Routing fix #70761

Credits

Huge thanks to @​ztanner, @​agadzik, @​huozhi, @​styfle, @​icyJoseph and @​wyattjoh for helping!