Skip to content

Commit ff794e4

Browse files
authored
chore: add e2e test for backwards ssh compatibility (coder#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

Lines changed: 139 additions & 10 deletions
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

Lines changed: 52 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 56 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy