Skip to content

Commit d6b7f42

Browse files
committed
Merge branch 'main' of github.com:coder/coder into groups
2 parents cba7065 + 27c8345 commit d6b7f42

File tree

17 files changed

+121
-24
lines changed

17 files changed

+121
-24
lines changed

agent/apphealth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, workspaceAgentApps Worksp
109109
mu.Unlock()
110110
}
111111

112-
t.Reset(time.Duration(app.Healthcheck.Interval))
112+
t.Reset(time.Duration(app.Healthcheck.Interval) * time.Second)
113113
}
114114
}()
115115
}

agent/apphealth_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66
"net/http/httptest"
77
"sync"
8+
"sync/atomic"
89
"testing"
910
"time"
1011

@@ -129,6 +130,37 @@ func TestAppHealth(t *testing.T) {
129130
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
130131
}, testutil.WaitLong, testutil.IntervalSlow)
131132
})
133+
134+
t.Run("NotSpamming", func(t *testing.T) {
135+
t.Parallel()
136+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
137+
defer cancel()
138+
apps := []codersdk.WorkspaceApp{
139+
{
140+
Name: "app2",
141+
Healthcheck: codersdk.Healthcheck{
142+
// URL: We don't set the URL for this test because the setup will
143+
// create a httptest server for us and set it for us.
144+
Interval: 1,
145+
Threshold: 1,
146+
},
147+
Health: codersdk.WorkspaceAppHealthInitializing,
148+
},
149+
}
150+
151+
var counter = new(int32)
152+
handlers := []http.Handler{
153+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
154+
atomic.AddInt32(counter, 1)
155+
}),
156+
}
157+
_, closeFn := setupAppReporter(ctx, t, apps, handlers)
158+
defer closeFn()
159+
// Ensure we haven't made more than 2 (expected 1 + 1 for buffer) requests in the last second.
160+
// if there is a bug where we are spamming the healthcheck route this will catch it.
161+
time.Sleep(time.Second)
162+
require.LessOrEqual(t, *counter, int32(2))
163+
})
132164
}
133165

134166
func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.WorkspaceApp, handlers []http.Handler) (agent.WorkspaceAgentApps, func()) {

coderd/database/databasefake/databasefake.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.G
315315
defer q.mutex.RUnlock()
316316

317317
for _, user := range q.users {
318-
if (user.Email == arg.Email || user.Username == arg.Username) && user.Deleted == arg.Deleted {
318+
if (strings.EqualFold(user.Email, arg.Email) || strings.EqualFold(user.Username, arg.Username)) && user.Deleted == arg.Deleted {
319319
return user, nil
320320
}
321321
}

coderd/database/dump.sql

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

coderd/database/migrations/000054_email_case.down.sql

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE UNIQUE INDEX IF NOT EXISTS users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false);

coderd/database/queries.sql.go

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

coderd/database/queries/users.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ SELECT
1717
FROM
1818
users
1919
WHERE
20-
(LOWER(username) = LOWER(@username) OR email = @email)
20+
(LOWER(username) = LOWER(@username) OR LOWER(email) = LOWER(@email))
2121
AND deleted = @deleted
2222
LIMIT
2323
1;

coderd/database/unique_constraint.go

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

coderd/users_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,19 @@ func TestPostLogin(t *testing.T) {
256256
}
257257
_, err := client.CreateFirstUser(ctx, req)
258258
require.NoError(t, err)
259+
259260
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
260261
Email: req.Email,
261262
Password: req.Password,
262263
})
263264
require.NoError(t, err)
265+
266+
// Login should be case insensitive
267+
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
268+
Email: strings.ToUpper(req.Email),
269+
Password: req.Password,
270+
})
271+
require.NoError(t, err)
264272
})
265273

266274
t.Run("Lifetime&Expire", func(t *testing.T) {

docs/images/hero-image.png

485 KB
Loading

docs/networking/port-forwarding.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ For more examples, see `coder port-forward --help`.
2525
## Dashboard
2626

2727
> To enable port forwarding via the dashboard, Coder must be configured with a
28-
> [wildcard access URL](./admin/configure#wildcard-access-url).
28+
> [wildcard access URL](../admin/configure.md#wildcard-access-url).
2929
3030
Use the "Port forward" button in the dashboard to access ports
3131
running on your workspace.

examples/lima/coder.yaml

+6-6
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ provision:
6363
#!/bin/bash
6464
set -eux -o pipefail
6565
command -v terraform >/dev/null 2>&1 && exit 0
66-
wget -qO - terraform.gpg https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/terraform-archive-keyring.gpg
67-
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/terraform-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" > /etc/apt/sources.list.d/terraform.list
68-
export DEBIAN_FRONTEND=noninteractive
69-
apt-get update -y
70-
apt-get install terraform=1.1.9
71-
apt-mark hold terraform
66+
DEBIAN_FRONTEND=noninteractive apt-get install -qqy unzip
67+
rm -fv /tmp/terraform.zip || true
68+
wget -qO /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.3.0/terraform_1.3.0_linux_$(dpkg --print-architecture).zip"
69+
unzip /tmp/terraform.zip -d /usr/local/bin/
70+
chmod +x /usr/local/bin/terraform
71+
rm -fv /tmp/terraform.zip || true
7272
- mode: system
7373
script: |
7474
#!/bin/bash

scripts/rules.go

+28
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package gorules
1717

1818
import (
1919
"github.com/quasilyte/go-ruleguard/dsl"
20+
"github.com/quasilyte/go-ruleguard/dsl/types"
2021
)
2122

2223
// Use xerrors everywhere! It provides additional stacktrace info!
@@ -238,3 +239,30 @@ func ProperRBACReturn(m dsl.Matcher) {
238239
}
239240
`).Report("Must write to 'ResponseWriter' before returning'")
240241
}
242+
243+
// FullResponseWriter ensures that any overridden response writer has full
244+
// functionality. Mainly is hijackable and flushable.
245+
func FullResponseWriter(m dsl.Matcher) {
246+
m.Match(`
247+
type $w struct {
248+
$*_
249+
http.ResponseWriter
250+
$*_
251+
}
252+
`).
253+
At(m["w"]).
254+
Where(m["w"].Filter(notImplementsFullResponseWriter)).
255+
Report("ResponseWriter \"$w\" must implement http.Flusher and http.Hijacker")
256+
}
257+
258+
// notImplementsFullResponseWriter returns false if the type does not implement
259+
// http.Flusher, http.Hijacker, and http.ResponseWriter.
260+
func notImplementsFullResponseWriter(ctx *dsl.VarFilterContext) bool {
261+
flusher := ctx.GetInterface(`net/http.Flusher`)
262+
hijacker := ctx.GetInterface(`net/http.Hijacker`)
263+
writer := ctx.GetInterface(`net/http.ResponseWriter`)
264+
p := types.NewPointer(ctx.Type)
265+
return !(types.Implements(p, writer) || types.Implements(ctx.Type, writer)) ||
266+
!(types.Implements(p, flusher) || types.Implements(ctx.Type, flusher)) ||
267+
!(types.Implements(p, hijacker) || types.Implements(ctx.Type, hijacker))
268+
}

site/src/components/Markdown/Markdown.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ export const Markdown: FC<{ children: string }> = ({ children }) => {
9595

9696
const useStyles = makeStyles((theme) => ({
9797
codeWithoutLanguage: {
98-
display: "block",
9998
overflowX: "auto",
10099
padding: "0.5em",
101100
background: theme.palette.background.default,

site/src/components/UserAutocomplete/UserAutocomplete.tsx

+27-7
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { useMachine } from "@xstate/react"
66
import { User } from "api/typesGenerated"
77
import { AvatarData } from "components/AvatarData/AvatarData"
88
import debounce from "just-debounce-it"
9-
import { ChangeEvent, useState } from "react"
9+
import { ChangeEvent, useEffect, useState } from "react"
1010
import { searchUserMachine } from "xServices/users/searchUserXService"
1111

1212
export type UserAutocompleteProps = {
13-
value: User | null
13+
value?: User | null
1414
onChange: (user: User | null) => void
1515
}
1616

@@ -19,24 +19,38 @@ export const UserAutocomplete: React.FC<UserAutocompleteProps> = ({ value, onCha
1919
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
2020
const [searchState, sendSearch] = useMachine(searchUserMachine)
2121
const { searchResults } = searchState.context
22+
const [selectedValue, setSelectedValue] = useState<User | null>(value || null)
23+
24+
// seed list of options on the first page load if a user pases in a value
25+
// since some organizations have long lists of users, we do not load all options on page load.
26+
useEffect(() => {
27+
if (value) {
28+
sendSearch("SEARCH", { query: value.email })
29+
}
30+
// eslint-disable-next-line react-hooks/exhaustive-deps
31+
}, [])
2232

2333
const handleFilterChange = debounce((event: ChangeEvent<HTMLInputElement>) => {
2434
sendSearch("SEARCH", { query: event.target.value })
2535
}, 1000)
2636

2737
return (
2838
<Autocomplete
29-
value={value}
39+
value={selectedValue}
3040
id="user-autocomplete"
31-
style={{ width: 300 }}
3241
open={isAutocompleteOpen}
3342
onOpen={() => {
3443
setIsAutocompleteOpen(true)
3544
}}
3645
onClose={() => {
3746
setIsAutocompleteOpen(false)
3847
}}
39-
onChange={(event, newValue) => {
48+
onChange={(_, newValue) => {
49+
if (newValue === null) {
50+
sendSearch("CLEAR_RESULTS")
51+
}
52+
53+
setSelectedValue(newValue)
4054
onChange(newValue)
4155
}}
4256
getOptionSelected={(option: User, value: User) => option.username === value.username}
@@ -84,10 +98,16 @@ export const UserAutocomplete: React.FC<UserAutocompleteProps> = ({ value, onCha
8498
export const useStyles = makeStyles((theme) => {
8599
return {
86100
autocomplete: {
101+
width: "100%",
102+
103+
"& .MuiFormControl-root": {
104+
width: "100%",
105+
},
106+
87107
"& .MuiInputBase-root": {
88-
width: 300,
108+
width: "100%",
89109
// Match button small height
90-
height: 36,
110+
height: 40,
91111
},
92112

93113
"& input": {

site/src/xServices/users/searchUserXService.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ import { User } from "api/typesGenerated"
33
import { queryToFilter } from "util/filters"
44
import { assign, createMachine } from "xstate"
55

6+
export type AutocompleteEvent = { type: "SEARCH"; query: string } | { type: "CLEAR_RESULTS" }
7+
68
export const searchUserMachine = createMachine(
79
{
810
id: "searchUserMachine",
911
schema: {
1012
context: {} as {
11-
searchResults: User[]
12-
},
13-
events: {} as {
14-
type: "SEARCH"
15-
query: string
13+
searchResults?: User[]
1614
},
15+
events: {} as AutocompleteEvent,
1716
services: {} as {
1817
searchUsers: {
1918
data: User[]
@@ -29,6 +28,10 @@ export const searchUserMachine = createMachine(
2928
idle: {
3029
on: {
3130
SEARCH: "searching",
31+
CLEAR_RESULTS: {
32+
actions: ["clearResults"],
33+
target: "idle",
34+
},
3235
},
3336
},
3437
searching: {
@@ -50,6 +53,9 @@ export const searchUserMachine = createMachine(
5053
assignSearchResults: assign({
5154
searchResults: (_, { data }) => data,
5255
}),
56+
clearResults: assign({
57+
searchResults: (_) => undefined,
58+
}),
5359
},
5460
},
5561
)

0 commit comments

Comments
 (0)