Skip to content

chore: add e2e test for backwards ssh compatibility #8761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 139 additions & 10 deletions site/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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)[]
Expand Down Expand Up @@ -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 changes: 52 additions & 0 deletions site/e2e/tests/outdatedAgent.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
57 changes: 56 additions & 1 deletion site/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down