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:
Kyle Carberry
2023-07-27 10:42:11 -05:00
committed by GitHub
parent 34dfbfa9d3
commit ff794e4b23
4 changed files with 249 additions and 11 deletions
+139 -10
View File
@@ -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
}
+52
View File
@@ -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()
})
})
})
})
+2
View File
@@ -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
View File
@@ -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"