Skip to content

Commit c801d46

Browse files
committed
Merge branch 'main' into 7452-banner-legacy-params
2 parents db4b4de + 8f768f8 commit c801d46

22 files changed

+347
-28
lines changed

coderd/coderd.go

+11
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,17 @@ func New(options *Options) *API {
806806
return []string{}
807807
})
808808
r.NotFound(cspMW(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP))).ServeHTTP)
809+
810+
// This must be before all middleware to improve the response time.
811+
// So make a new router, and mount the old one as the root.
812+
rootRouter := chi.NewRouter()
813+
// This is the only route we add before all the middleware.
814+
// We want to time the latency of the request, so any middleware will
815+
// interfere with that timing.
816+
rootRouter.Get("/latency-check", LatencyCheck(api.AccessURL))
817+
rootRouter.Mount("/", r)
818+
api.RootHandler = rootRouter
819+
809820
return api
810821
}
811822

coderd/coderd_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ func TestDERPLatencyCheck(t *testing.T) {
124124
require.Equal(t, http.StatusOK, res.StatusCode)
125125
}
126126

127+
func TestFastLatencyCheck(t *testing.T) {
128+
t.Parallel()
129+
client := coderdtest.New(t, nil)
130+
res, err := client.Request(context.Background(), http.MethodGet, "/latency-check", nil)
131+
require.NoError(t, err)
132+
defer res.Body.Close()
133+
require.Equal(t, http.StatusOK, res.StatusCode)
134+
}
135+
127136
func TestHealthz(t *testing.T) {
128137
t.Parallel()
129138
client := coderdtest.New(t, nil)

coderd/latencycheck.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package coderd
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
"strings"
7+
)
8+
9+
func LatencyCheck(allowedOrigins ...*url.URL) http.HandlerFunc {
10+
allowed := make([]string, 0, len(allowedOrigins))
11+
for _, origin := range allowedOrigins {
12+
// Allow the origin without a path
13+
tmp := *origin
14+
tmp.Path = ""
15+
allowed = append(allowed, strings.TrimSuffix(origin.String(), "/"))
16+
}
17+
origins := strings.Join(allowed, ",")
18+
return func(rw http.ResponseWriter, r *http.Request) {
19+
// Allowing timing information to be shared. This allows the browser
20+
// to exclude TLS handshake timing.
21+
rw.Header().Set("Timing-Allow-Origin", origins)
22+
rw.WriteHeader(http.StatusOK)
23+
}
24+
}

enterprise/coderd/workspaceproxy_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ func TestRegions(t *testing.T) {
175175
})
176176

177177
t.Run("GoingAway", func(t *testing.T) {
178+
t.Skip("This is flakey in CI because it relies on internal go routine timing. Should refactor.")
178179
t.Parallel()
179180

180181
dv := coderdtest.DeploymentValues(t)

enterprise/wsproxy/wsproxy.go

+24-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"cdr.dev/slog"
2121
"github.com/coder/coder/buildinfo"
22+
"github.com/coder/coder/coderd"
2223
"github.com/coder/coder/coderd/httpapi"
2324
"github.com/coder/coder/coderd/httpmw"
2425
"github.com/coder/coder/coderd/tracing"
@@ -186,6 +187,21 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
186187
SecureAuthCookie: opts.SecureAuthCookie,
187188
}
188189

190+
// The primary coderd dashboard needs to make some GET requests to
191+
// the workspace proxies to check latency.
192+
corsMW := cors.Handler(cors.Options{
193+
AllowedOrigins: []string{
194+
// Allow the dashboard to make requests to the proxy for latency
195+
// checks.
196+
opts.DashboardURL.String(),
197+
},
198+
// Only allow GET requests for latency checks.
199+
AllowedMethods: []string{http.MethodOptions, http.MethodGet},
200+
AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"},
201+
// Do not send any cookies
202+
AllowCredentials: false,
203+
})
204+
189205
// Routes
190206
apiRateLimiter := httpmw.RateLimit(opts.APIRateLimit, time.Minute)
191207
// Persistent middlewares to all routes
@@ -198,20 +214,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
198214
httpmw.ExtractRealIP(s.Options.RealIPConfig),
199215
httpmw.Logger(s.Logger),
200216
httpmw.Prometheus(s.PrometheusRegistry),
201-
// The primary coderd dashboard needs to make some GET requests to
202-
// the workspace proxies to check latency.
203-
cors.Handler(cors.Options{
204-
AllowedOrigins: []string{
205-
// Allow the dashboard to make requests to the proxy for latency
206-
// checks.
207-
opts.DashboardURL.String(),
208-
},
209-
// Only allow GET requests for latency checks.
210-
AllowedMethods: []string{http.MethodGet},
211-
AllowedHeaders: []string{"Accept", "Content-Type"},
212-
// Do not send any cookies
213-
AllowCredentials: false,
214-
}),
217+
corsMW,
215218

216219
// HandleSubdomain is a middleware that handles all requests to the
217220
// subdomain-based workspace apps.
@@ -260,6 +263,13 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
260263
})
261264
})
262265

266+
// See coderd/coderd.go for why we need this.
267+
rootRouter := chi.NewRouter()
268+
// Make sure to add the cors middleware to the latency check route.
269+
rootRouter.Get("/latency-check", corsMW(coderd.LatencyCheck(s.DashboardURL, s.AppServer.AccessURL)).ServeHTTP)
270+
rootRouter.Mount("/", r)
271+
s.Handler = rootRouter
272+
263273
return s, nil
264274
}
265275

site/jest.setup.ts

+24
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,33 @@ import "jest-location-mock"
66
import { TextEncoder, TextDecoder } from "util"
77
import { Blob } from "buffer"
88
import jestFetchMock from "jest-fetch-mock"
9+
import { ProxyLatencyReport } from "contexts/useProxyLatency"
10+
import { RegionsResponse } from "api/typesGenerated"
911

1012
jestFetchMock.enableMocks()
1113

14+
// useProxyLatency does some http requests to determine latency.
15+
// This would fail unit testing, or at least make it very slow with
16+
// actual network requests. So just globally mock this hook.
17+
jest.mock("contexts/useProxyLatency", () => ({
18+
useProxyLatency: (proxies?: RegionsResponse) => {
19+
if (!proxies) {
20+
return {} as Record<string, ProxyLatencyReport>
21+
}
22+
23+
return proxies.regions.reduce((acc, proxy) => {
24+
acc[proxy.id] = {
25+
accurate: true,
26+
// Return a constant latency of 8ms.
27+
// If you make this random it could break stories.
28+
latencyMS: 8,
29+
at: new Date(),
30+
}
31+
return acc
32+
}, {} as Record<string, ProxyLatencyReport>)
33+
},
34+
}))
35+
1236
global.TextEncoder = TextEncoder
1337
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
1438
global.TextDecoder = TextDecoder as any

site/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"dependencies": {
3131
"@emoji-mart/data": "1.0.5",
3232
"@emoji-mart/react": "1.0.1",
33+
"@fastly/performance-observer-polyfill": "^2.0.0",
3334
"@emotion/react": "^11.10.8",
3435
"@emotion/styled": "^11.10.8",
3536
"@fontsource/ibm-plex-mono": "4.5.10",

site/src/components/AppLink/AppLink.stories.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MockWorkspace,
66
MockWorkspaceAgent,
77
MockWorkspaceApp,
8+
MockProxyLatencies,
89
} from "testHelpers/entities"
910
import { AppLink, AppLinkProps } from "./AppLink"
1011
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
@@ -17,6 +18,7 @@ export default {
1718
const Template: Story<AppLinkProps> = (args) => (
1819
<ProxyContext.Provider
1920
value={{
21+
proxyLatencies: MockProxyLatencies,
2022
proxy: getPreferredProxy(MockWorkspaceProxies, MockPrimaryWorkspaceProxy),
2123
proxies: MockWorkspaceProxies,
2224
isLoading: false,

site/src/components/Resources/AgentLatency.tsx

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useRef, useState, FC } from "react"
22
import { makeStyles, useTheme } from "@mui/styles"
3+
import { Theme } from "@mui/material/styles"
34
import {
45
HelpTooltipText,
56
HelpPopover,
67
HelpTooltipTitle,
78
} from "components/Tooltips/HelpTooltip"
89
import { Stack } from "components/Stack/Stack"
910
import { WorkspaceAgent, DERPRegion } from "api/typesGenerated"
10-
import { Theme } from "@mui/material/styles"
11+
import { getLatencyColor } from "utils/colors"
1112

1213
const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
1314
// Find the right latency to display
@@ -22,17 +23,9 @@ const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
2223
return undefined
2324
}
2425

25-
// Get the color
26-
let color = theme.palette.success.light
27-
if (latency.latency_ms >= 150 && latency.latency_ms < 300) {
28-
color = theme.palette.warning.light
29-
} else if (latency.latency_ms >= 300) {
30-
color = theme.palette.error.light
31-
}
32-
3326
return {
3427
...latency,
35-
color,
28+
color: getLatencyColor(theme, latency.latency_ms),
3629
}
3730
}
3831

site/src/components/Resources/AgentRow.stories.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
MockWorkspaceAgentStartTimeout,
1717
MockWorkspaceAgentTimeout,
1818
MockWorkspaceApp,
19+
MockProxyLatencies,
1920
} from "testHelpers/entities"
2021
import { AgentRow, AgentRowProps } from "./AgentRow"
2122
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
@@ -56,6 +57,7 @@ const TemplateFC = (
5657
return (
5758
<ProxyContext.Provider
5859
value={{
60+
proxyLatencies: MockProxyLatencies,
5961
proxy: getPreferredProxy(proxies, selectedProxy),
6062
proxies: proxies,
6163
isLoading: false,

site/src/components/Resources/ResourceCard.stories.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { action } from "@storybook/addon-actions"
22
import { Story } from "@storybook/react"
3-
import { MockWorkspace, MockWorkspaceResource } from "testHelpers/entities"
3+
import {
4+
MockProxyLatencies,
5+
MockWorkspace,
6+
MockWorkspaceResource,
7+
} from "testHelpers/entities"
48
import { AgentRow } from "./AgentRow"
59
import { ResourceCard, ResourceCardProps } from "./ResourceCard"
610
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
@@ -18,6 +22,7 @@ Example.args = {
1822
agentRow: (agent) => (
1923
<ProxyContext.Provider
2024
value={{
25+
proxyLatencies: MockProxyLatencies,
2126
proxy: getPreferredProxy([], undefined),
2227
proxies: [],
2328
isLoading: false,
@@ -84,6 +89,7 @@ BunchOfMetadata.args = {
8489
agentRow: (agent) => (
8590
<ProxyContext.Provider
8691
value={{
92+
proxyLatencies: MockProxyLatencies,
8793
proxy: getPreferredProxy([], undefined),
8894
proxies: [],
8995
isLoading: false,

site/src/components/Workspace/Workspace.stories.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace"
77
import { withReactContext } from "storybook-react-context"
88
import EventSource from "eventsourcemock"
99
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
10+
import { MockProxyLatencies } from "../../testHelpers/entities"
1011

1112
export default {
1213
title: "components/Workspace",
@@ -26,6 +27,7 @@ export default {
2627
const Template: Story<WorkspaceProps> = (args) => (
2728
<ProxyContext.Provider
2829
value={{
30+
proxyLatencies: MockProxyLatencies,
2931
proxy: getPreferredProxy([], undefined),
3032
proxies: [],
3133
isLoading: false,

site/src/contexts/ProxyContext.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
useContext,
1010
useState,
1111
} from "react"
12+
import { ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"
1213

1314
interface ProxyContextValue {
1415
proxy: PreferredProxy
1516
proxies?: Region[]
17+
proxyLatencies?: Record<string, ProxyLatencyReport>
1618
// isfetched is true when the proxy api call is complete.
1719
isFetched: boolean
1820
// isLoading is true if the proxy is in the process of being fetched.
@@ -72,6 +74,10 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
7274
},
7375
})
7476

77+
// Everytime we get a new proxiesResponse, update the latency check
78+
// to each workspace proxy.
79+
const proxyLatencies = useProxyLatency(proxiesResp)
80+
7581
const setAndSaveProxy = (
7682
selectedProxy?: Region,
7783
// By default the proxies come from the api call above.
@@ -95,6 +101,7 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
95101
return (
96102
<ProxyContext.Provider
97103
value={{
104+
proxyLatencies: proxyLatencies,
98105
proxy: experimentEnabled
99106
? proxy
100107
: {

0 commit comments

Comments
 (0)