mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add e2e test for backwards ssh compatibility (#8761)
* chore: add e2e test for backwards ssh compatibility * Use the SSH client directly * fmt
This commit is contained in:
+139
-10
@@ -15,6 +15,8 @@ import {
|
||||
Resource,
|
||||
} from "./provisionerGenerated"
|
||||
import { port } from "./playwright.config"
|
||||
import * as ssh from "ssh2"
|
||||
import { Duplex } from "stream"
|
||||
|
||||
// createWorkspace creates a workspace for a template.
|
||||
// It does not wait for it to be running, but it does navigate to the page.
|
||||
@@ -59,19 +61,125 @@ export const createTemplate = async (
|
||||
return name
|
||||
}
|
||||
|
||||
// sshIntoWorkspace spawns a Coder SSH process and a client connected to it.
|
||||
export const sshIntoWorkspace = async (
|
||||
page: Page,
|
||||
workspace: string,
|
||||
): Promise<ssh.Client> => {
|
||||
const sessionToken = await findSessionToken(page)
|
||||
return new Promise<ssh.Client>((resolve, reject) => {
|
||||
const cp = spawn(
|
||||
"go",
|
||||
["run", coderMainPath(), "ssh", "--stdio", workspace],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
CODER_SESSION_TOKEN: sessionToken,
|
||||
CODER_URL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
)
|
||||
cp.on("error", (err) => reject(err))
|
||||
const proxyStream = new Duplex({
|
||||
read: (size) => {
|
||||
return cp.stdout.read(Math.min(size, cp.stdout.readableLength))
|
||||
},
|
||||
write: cp.stdin.write.bind(cp.stdin),
|
||||
})
|
||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||
cp.stderr.on("data", (data) => console.log(data.toString()))
|
||||
cp.stdout.on("readable", (...args) => {
|
||||
proxyStream.emit("readable", ...args)
|
||||
if (cp.stdout.readableLength > 0) {
|
||||
proxyStream.emit("data", cp.stdout.read())
|
||||
}
|
||||
})
|
||||
const client = new ssh.Client()
|
||||
client.connect({
|
||||
sock: proxyStream,
|
||||
username: "coder",
|
||||
})
|
||||
client.on("error", (err) => reject(err))
|
||||
client.on("ready", () => {
|
||||
resolve(client)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// startAgent runs the coder agent with the provided token.
|
||||
// It awaits the agent to be ready before returning.
|
||||
export const startAgent = async (page: Page, token: string): Promise<void> => {
|
||||
const coderMain = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"enterprise",
|
||||
"cmd",
|
||||
"coder",
|
||||
"main.go",
|
||||
)
|
||||
const cp = spawn("go", ["run", coderMain, "agent", "--no-reap"], {
|
||||
return startAgentWithCommand(page, token, "go", "run", coderMainPath())
|
||||
}
|
||||
|
||||
// downloadCoderVersion downloads the version provided into a temporary dir and
|
||||
// caches it so subsequent calls are fast.
|
||||
export const downloadCoderVersion = async (
|
||||
version: string,
|
||||
): Promise<string> => {
|
||||
if (version.startsWith("v")) {
|
||||
version = version.slice(1)
|
||||
}
|
||||
|
||||
const binaryName = "coder-e2e-" + version
|
||||
const tempDir = "/tmp"
|
||||
// The install script adds `./bin` automatically to the path :shrug:
|
||||
const binaryPath = path.join(tempDir, "bin", binaryName)
|
||||
|
||||
const exists = await new Promise<boolean>((resolve) => {
|
||||
const cp = spawn(binaryPath, ["version"])
|
||||
cp.on("close", (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
cp.on("error", () => resolve(false))
|
||||
})
|
||||
if (exists) {
|
||||
return binaryPath
|
||||
}
|
||||
|
||||
// Runs our public install script using our options to
|
||||
// install the binary!
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cp = spawn("sh", [
|
||||
"-c",
|
||||
[
|
||||
"curl",
|
||||
"-L",
|
||||
"https://coder.com/install.sh",
|
||||
"|",
|
||||
"sh",
|
||||
"-s",
|
||||
"--",
|
||||
"--version",
|
||||
version,
|
||||
"--method",
|
||||
"standalone",
|
||||
"--prefix",
|
||||
tempDir,
|
||||
"--binary-name",
|
||||
binaryName,
|
||||
].join(" "),
|
||||
])
|
||||
// eslint-disable-next-line no-console -- Needed for debugging
|
||||
cp.stderr.on("data", (data) => console.log(data.toString()))
|
||||
cp.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error("curl failed with code " + code))
|
||||
}
|
||||
})
|
||||
})
|
||||
return binaryPath
|
||||
}
|
||||
|
||||
export const startAgentWithCommand = async (
|
||||
page: Page,
|
||||
token: string,
|
||||
command: string,
|
||||
...args: string[]
|
||||
): Promise<void> => {
|
||||
const cp = spawn(command, [...args, "agent", "--no-reap"], {
|
||||
env: {
|
||||
...process.env,
|
||||
CODER_AGENT_URL: "http://localhost:" + port,
|
||||
@@ -90,6 +198,18 @@ export const startAgent = async (page: Page, token: string): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
const coderMainPath = (): string => {
|
||||
return path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"enterprise",
|
||||
"cmd",
|
||||
"coder",
|
||||
"main.go",
|
||||
)
|
||||
}
|
||||
|
||||
// Allows users to more easily define properties they want for agents and resources!
|
||||
type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
@@ -259,3 +379,12 @@ export const createServer = async (
|
||||
await new Promise<void>((r) => e.listen(port, r))
|
||||
return e
|
||||
}
|
||||
|
||||
const findSessionToken = async (page: Page): Promise<string> => {
|
||||
const cookies = await page.context().cookies()
|
||||
const sessionCookie = cookies.find((c) => c.name === "coder_session_token")
|
||||
if (!sessionCookie) {
|
||||
throw new Error("session token not found")
|
||||
}
|
||||
return sessionCookie.value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { test } from "@playwright/test"
|
||||
import { randomUUID } from "crypto"
|
||||
import {
|
||||
createTemplate,
|
||||
createWorkspace,
|
||||
downloadCoderVersion,
|
||||
sshIntoWorkspace,
|
||||
startAgentWithCommand,
|
||||
} from "../helpers"
|
||||
|
||||
const agentVersion = "v0.14.0"
|
||||
|
||||
test("ssh with agent " + agentVersion, async ({ page }) => {
|
||||
const token = randomUUID()
|
||||
const template = await createTemplate(page, {
|
||||
apply: [
|
||||
{
|
||||
complete: {
|
||||
resources: [
|
||||
{
|
||||
agents: [
|
||||
{
|
||||
token,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const workspace = await createWorkspace(page, template)
|
||||
const binaryPath = await downloadCoderVersion(agentVersion)
|
||||
await startAgentWithCommand(page, token, binaryPath)
|
||||
|
||||
const client = await sshIntoWorkspace(page, workspace)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// We just exec a command to be certain the agent is running!
|
||||
client.exec("exit 0", (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
stream.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
return reject(new Error(`Command exited with code ${code}`))
|
||||
}
|
||||
client.end()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -127,6 +127,7 @@
|
||||
"@types/react-syntax-highlighter": "15.5.5",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/ssh2": "1.11.13",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/uuid": "9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.1.0",
|
||||
@@ -154,6 +155,7 @@
|
||||
"msw": "1.2.2",
|
||||
"prettier": "3.0.0",
|
||||
"resize-observer": "1.0.4",
|
||||
"ssh2": "1.14.0",
|
||||
"storybook": "7.1.0",
|
||||
"storybook-addon-react-router-v6": "1.0.2",
|
||||
"storybook-react-context": "0.6.0",
|
||||
|
||||
+56
-1
@@ -3751,6 +3751,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.23.tgz#b6e934fe427eb7081d0015aad070acb3373c3c90"
|
||||
integrity sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.1.tgz#84c32903bf3a09f7878c391d31ff08f6fe7d8335"
|
||||
integrity sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||
@@ -3905,6 +3910,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ssh2@1.11.13":
|
||||
version "1.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.13.tgz#e6224da936abec0541bf26aa826b1cc37ea70d69"
|
||||
integrity sha512-08WbG68HvQ2YVi74n2iSUnYHYpUdFc/s2IsI0BHBdJwaqYJpWlVv9elL0tYShTv60yr0ObdxJR5NrCRiGJ/0CQ==
|
||||
dependencies:
|
||||
"@types/node" "^18.11.18"
|
||||
|
||||
"@types/stack-utils@^2.0.0":
|
||||
version "2.0.1"
|
||||
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:
|
||||
es-shim-unscopables "^1.0.0"
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
asn1@^0.2.6:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
|
||||
dependencies:
|
||||
safer-buffer "~2.1.0"
|
||||
|
||||
assert@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
|
||||
@@ -4632,6 +4651,13 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bcrypt-pbkdf@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
better-opn@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
|
||||
@@ -4762,6 +4788,11 @@ buffer@^5.5.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buildcheck@~0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
|
||||
integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
|
||||
|
||||
builtin-modules@^3.3.0:
|
||||
version "3.3.0"
|
||||
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:
|
||||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
cpu-features@~0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.8.tgz#a2d464b023b8ad09004c8cdca23b33f192f63546"
|
||||
integrity sha512-BbHBvtYhUhksqTjr6bhNOjGgMnhwhGTQmOoZGD+K7BCaQDCuZl/Ve1ZxUSMRwVC4D/rkCPQ2MAIeYzrWyK7eEg==
|
||||
dependencies:
|
||||
buildcheck "~0.0.6"
|
||||
nan "^2.17.0"
|
||||
|
||||
create-jest-runner@^0.11.2:
|
||||
version "0.11.2"
|
||||
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:
|
||||
get-intrinsic "^1.1.3"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -10827,6 +10866,17 @@ sprintf-js@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
|
||||
|
||||
ssh2@1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.14.0.tgz#8f68440e1b768b66942c9e4e4620b2725b3555bb"
|
||||
integrity sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==
|
||||
dependencies:
|
||||
asn1 "^0.2.6"
|
||||
bcrypt-pbkdf "^1.0.2"
|
||||
optionalDependencies:
|
||||
cpu-features "~0.0.8"
|
||||
nan "^2.17.0"
|
||||
|
||||
stack-generator@^2.0.5:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
|
||||
@@ -11419,6 +11469,11 @@ tween-functions@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||
integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==
|
||||
|
||||
tweetnacl@^0.14.3:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
|
||||
Reference in New Issue
Block a user