Skip to content

Commit ff794e4

Browse files
authored
chore: add e2e test for backwards ssh compatibility (#8761)
* chore: add e2e test for backwards ssh compatibility * Use the SSH client directly * fmt
1 parent 34dfbfa commit ff794e4

File tree

4 files changed

+249
-11
lines changed

4 files changed

+249
-11
lines changed

site/e2e/helpers.ts

+139-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
Resource,
1616
} from "./provisionerGenerated"
1717
import { port } from "./playwright.config"
18+
import * as ssh from "ssh2"
19+
import { Duplex } from "stream"
1820

1921
// createWorkspace creates a workspace for a template.
2022
// It does not wait for it to be running, but it does navigate to the page.
@@ -59,19 +61,125 @@ export const createTemplate = async (
5961
return name
6062
}
6163

64+
// sshIntoWorkspace spawns a Coder SSH process and a client connected to it.
65+
export const sshIntoWorkspace = async (
66+
page: Page,
67+
workspace: string,
68+
): Promise<ssh.Client> => {
69+
const sessionToken = await findSessionToken(page)
70+
return new Promise<ssh.Client>((resolve, reject) => {
71+
const cp = spawn(
72+
"go",
73+
["run", coderMainPath(), "ssh", "--stdio", workspace],
74+
{
75+
env: {
76+
...process.env,
77+
CODER_SESSION_TOKEN: sessionToken,
78+
CODER_URL: "http://localhost:3000",
79+
},
80+
},
81+
)
82+
cp.on("error", (err) => reject(err))
83+
const proxyStream = new Duplex({
84+
read: (size) => {
85+
return cp.stdout.read(Math.min(size, cp.stdout.readableLength))
86+
},
87+
write: cp.stdin.write.bind(cp.stdin),
88+
})
89+
// eslint-disable-next-line no-console -- Helpful for debugging
90+
cp.stderr.on("data", (data) => console.log(data.toString()))
91+
cp.stdout.on("readable", (...args) => {
92+
proxyStream.emit("readable", ...args)
93+
if (cp.stdout.readableLength > 0) {
94+
proxyStream.emit("data", cp.stdout.read())
95+
}
96+
})
97+
const client = new ssh.Client()
98+
client.connect({
99+
sock: proxyStream,
100+
username: "coder",
101+
})
102+
client.on("error", (err) => reject(err))
103+
client.on("ready", () => {
104+
resolve(client)
105+
})
106+
})
107+
}
108+
62109
// startAgent runs the coder agent with the provided token.
63110
// It awaits the agent to be ready before returning.
64111
export const startAgent = async (page: Page, token: string): Promise<void> => {
65-
const coderMain = path.join(
66-
__dirname,
67-
"..",
68-
"..",
69-
"enterprise",
70-
"cmd",
71-
"coder",
72-
"main.go",
73-
)
74-
const cp = spawn("go", ["run", coderMain, "agent", "--no-reap"], {
112+
return startAgentWithCommand(page, token, "go", "run", coderMainPath())
113+
}
114+
115+
// downloadCoderVersion downloads the version provided into a temporary dir and
116+
// caches it so subsequent calls are fast.
117+
export const downloadCoderVersion = async (
118+
version: string,
119+
): Promise<string> => {
120+
if (version.startsWith("v")) {
121+
version = version.slice(1)
122+
}
123+
124+
const binaryName = "coder-e2e-" + version
125+
const tempDir = "/tmp"
126+
// The install script adds `./bin` automatically to the path :shrug:
127+
const binaryPath = path.join(tempDir, "bin", binaryName)
128+
129+
const exists = await new Promise<boolean>((resolve) => {
130+
const cp = spawn(binaryPath, ["version"])
131+
cp.on("close", (code) => {
132+
resolve(code === 0)
133+
})
134+
cp.on("error", () => resolve(false))
135+
})
136+
if (exists) {
137+
return binaryPath
138+
}
139+
140+
// Runs our public install script using our options to
141+
// install the binary!
142+
await new Promise<void>((resolve, reject) => {
143+
const cp = spawn("sh", [
144+
"-c",
145+
[
146+
"curl",
147+
"-L",
148+
"https://coder.com/install.sh",
149+
"|",
150+
"sh",
151+
"-s",
152+
"--",
153+
"--version",
154+
version,
155+
"--method",
156+
"standalone",
157+
"--prefix",
158+
tempDir,
159+
"--binary-name",
160+
binaryName,
161+
].join(" "),
162+
])
163+
// eslint-disable-next-line no-console -- Needed for debugging
164+
cp.stderr.on("data", (data) => console.log(data.toString()))
165+
cp.on("close", (code) => {
166+
if (code === 0) {
167+
resolve()
168+
} else {
169+
reject(new Error("curl failed with code " + code))
170+
}
171+
})
172+
})
173+
return binaryPath
174+
}
175+
176+
export const startAgentWithCommand = async (
177+
page: Page,
178+
token: string,
179+
command: string,
180+
...args: string[]
181+
): Promise<void> => {
182+
const cp = spawn(command, [...args, "agent", "--no-reap"], {
75183
env: {
76184
...process.env,
77185
CODER_AGENT_URL: "http://localhost:" + port,
@@ -90,6 +198,18 @@ export const startAgent = async (page: Page, token: string): Promise<void> => {
90198
}
91199
}
92200

201+
const coderMainPath = (): string => {
202+
return path.join(
203+
__dirname,
204+
"..",
205+
"..",
206+
"enterprise",
207+
"cmd",
208+
"coder",
209+
"main.go",
210+
)
211+
}
212+
93213
// Allows users to more easily define properties they want for agents and resources!
94214
type RecursivePartial<T> = {
95215
[P in keyof T]?: T[P] extends (infer U)[]
@@ -259,3 +379,12 @@ export const createServer = async (
259379
await new Promise<void>((r) => e.listen(port, r))
260380
return e
261381
}
382+
383+
const findSessionToken = async (page: Page): Promise<string> => {
384+
const cookies = await page.context().cookies()
385+
const sessionCookie = cookies.find((c) => c.name === "coder_session_token")
386+
if (!sessionCookie) {
387+
throw new Error("session token not found")
388+
}
389+
return sessionCookie.value
390+
}

site/e2e/tests/outdatedAgent.spec.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { test } from "@playwright/test"
2+
import { randomUUID } from "crypto"
3+
import {
4+
createTemplate,
5+
createWorkspace,
6+
downloadCoderVersion,
7+
sshIntoWorkspace,
8+
startAgentWithCommand,
9+
} from "../helpers"
10+
11+
const agentVersion = "v0.14.0"
12+
13+
test("ssh with agent " + agentVersion, async ({ page }) => {
14+
const token = randomUUID()
15+
const template = await createTemplate(page, {
16+
apply: [
17+
{
18+
complete: {
19+
resources: [
20+
{
21+
agents: [
22+
{
23+
token,
24+
},
25+
],
26+
},
27+
],
28+
},
29+
},
30+
],
31+
})
32+
const workspace = await createWorkspace(page, template)
33+
const binaryPath = await downloadCoderVersion(agentVersion)
34+
await startAgentWithCommand(page, token, binaryPath)
35+
36+
const client = await sshIntoWorkspace(page, workspace)
37+
await new Promise<void>((resolve, reject) => {
38+
// We just exec a command to be certain the agent is running!
39+
client.exec("exit 0", (err, stream) => {
40+
if (err) {
41+
return reject(err)
42+
}
43+
stream.on("exit", (code) => {
44+
if (code !== 0) {
45+
return reject(new Error(`Command exited with code ${code}`))
46+
}
47+
client.end()
48+
resolve()
49+
})
50+
})
51+
})
52+
})

site/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"@types/react-syntax-highlighter": "15.5.5",
128128
"@types/react-virtualized-auto-sizer": "1.0.1",
129129
"@types/react-window": "1.8.5",
130+
"@types/ssh2": "1.11.13",
130131
"@types/ua-parser-js": "0.7.36",
131132
"@types/uuid": "9.0.2",
132133
"@typescript-eslint/eslint-plugin": "6.1.0",
@@ -154,6 +155,7 @@
154155
"msw": "1.2.2",
155156
"prettier": "3.0.0",
156157
"resize-observer": "1.0.4",
158+
"ssh2": "1.14.0",
157159
"storybook": "7.1.0",
158160
"storybook-addon-react-router-v6": "1.0.2",
159161
"storybook-react-context": "0.6.0",

site/yarn.lock

+56-1
Original file line numberDiff line numberDiff line change
@@ -3751,6 +3751,11 @@
37513751
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.23.tgz#b6e934fe427eb7081d0015aad070acb3373c3c90"
37523752
integrity sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==
37533753

3754+
"@types/node@^18.11.18":
3755+
version "18.17.1"
3756+
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.1.tgz#84c32903bf3a09f7878c391d31ff08f6fe7d8335"
3757+
integrity sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw==
3758+
37543759
"@types/normalize-package-data@^2.4.0":
37553760
version "2.4.1"
37563761
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
@@ -3905,6 +3910,13 @@
39053910
dependencies:
39063911
"@types/node" "*"
39073912

3913+
"@types/ssh2@1.11.13":
3914+
version "1.11.13"
3915+
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.13.tgz#e6224da936abec0541bf26aa826b1cc37ea70d69"
3916+
integrity sha512-08WbG68HvQ2YVi74n2iSUnYHYpUdFc/s2IsI0BHBdJwaqYJpWlVv9elL0tYShTv60yr0ObdxJR5NrCRiGJ/0CQ==
3917+
dependencies:
3918+
"@types/node" "^18.11.18"
3919+
39083920
"@types/stack-utils@^2.0.0":
39093921
version "2.0.1"
39103922
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
@@ -4435,6 +4447,13 @@ array.prototype.tosorted@^1.1.1:
44354447
es-shim-unscopables "^1.0.0"
44364448
get-intrinsic "^1.1.3"
44374449

4450+
asn1@^0.2.6:
4451+
version "0.2.6"
4452+
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
4453+
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
4454+
dependencies:
4455+
safer-buffer "~2.1.0"
4456+
44384457
assert@^2.0.0:
44394458
version "2.0.0"
44404459
resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
@@ -4632,6 +4651,13 @@ base64-js@^1.3.1:
46324651
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
46334652
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
46344653

4654+
bcrypt-pbkdf@^1.0.2:
4655+
version "1.0.2"
4656+
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
4657+
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
4658+
dependencies:
4659+
tweetnacl "^0.14.3"
4660+
46354661
better-opn@^3.0.2:
46364662
version "3.0.2"
46374663
resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
@@ -4762,6 +4788,11 @@ buffer@^5.5.0:
47624788
base64-js "^1.3.1"
47634789
ieee754 "^1.1.13"
47644790

4791+
buildcheck@~0.0.6:
4792+
version "0.0.6"
4793+
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
4794+
integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
4795+
47654796
builtin-modules@^3.3.0:
47664797
version "3.3.0"
47674798
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
@@ -5238,6 +5269,14 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
52385269
path-type "^4.0.0"
52395270
yaml "^1.10.0"
52405271

5272+
cpu-features@~0.0.8:
5273+
version "0.0.8"
5274+
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.8.tgz#a2d464b023b8ad09004c8cdca23b33f192f63546"
5275+
integrity sha512-BbHBvtYhUhksqTjr6bhNOjGgMnhwhGTQmOoZGD+K7BCaQDCuZl/Ve1ZxUSMRwVC4D/rkCPQ2MAIeYzrWyK7eEg==
5276+
dependencies:
5277+
buildcheck "~0.0.6"
5278+
nan "^2.17.0"
5279+
52415280
create-jest-runner@^0.11.2:
52425281
version "0.11.2"
52435282
resolved "https://registry.yarnpkg.com/create-jest-runner/-/create-jest-runner-0.11.2.tgz#4b4f62ccef1e4de12e80f81c2cf8211fa392a962"
@@ -10563,7 +10602,7 @@ safe-regex-test@^1.0.0:
1056310602
get-intrinsic "^1.1.3"
1056410603
is-regex "^1.1.4"
1056510604

10566-
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
10605+
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0:
1056710606
version "2.1.2"
1056810607
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
1056910608
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -10827,6 +10866,17 @@ sprintf-js@~1.0.2:
1082710866
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
1082810867
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
1082910868

10869+
ssh2@1.14.0:
10870+
version "1.14.0"
10871+
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.14.0.tgz#8f68440e1b768b66942c9e4e4620b2725b3555bb"
10872+
integrity sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==
10873+
dependencies:
10874+
asn1 "^0.2.6"
10875+
bcrypt-pbkdf "^1.0.2"
10876+
optionalDependencies:
10877+
cpu-features "~0.0.8"
10878+
nan "^2.17.0"
10879+
1083010880
stack-generator@^2.0.5:
1083110881
version "2.0.10"
1083210882
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
@@ -11419,6 +11469,11 @@ tween-functions@^1.2.0:
1141911469
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
1142011470
integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==
1142111471

11472+
tweetnacl@^0.14.3:
11473+
version "0.14.5"
11474+
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
11475+
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
11476+
1142211477
type-check@^0.4.0, type-check@~0.4.0:
1142311478
version "0.4.0"
1142411479
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"

0 commit comments

Comments
 (0)