diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index f9d37e3190eee..3194192e6730c 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -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 => { + const sessionToken = await findSessionToken(page) + return new Promise((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 => { - 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 => { + 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((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((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 => { + 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 => { } } +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 = { [P in keyof T]?: T[P] extends (infer U)[] @@ -259,3 +379,12 @@ export const createServer = async ( await new Promise((r) => e.listen(port, r)) return e } + +const findSessionToken = async (page: Page): Promise => { + 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 +} diff --git a/site/e2e/tests/outdatedAgent.spec.ts b/site/e2e/tests/outdatedAgent.spec.ts new file mode 100644 index 0000000000000..2b88ea71110df --- /dev/null +++ b/site/e2e/tests/outdatedAgent.spec.ts @@ -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((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() + }) + }) + }) +}) diff --git a/site/package.json b/site/package.json index 2f9d37b79aa35..2005c1bfccd06 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/yarn.lock b/site/yarn.lock index 15b7a825e5832..eb84028401d4e 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -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" 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