diff --git a/.gitignore b/.gitignore index 616f9b01..0b810b29 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ release-images/ node_modules node-* /plugins +/lib/coder-cloud-agent diff --git a/package.json b/package.json index 4d75331e..bf5977c8 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/pem": "^1.9.5", "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", + "@types/split2": "^2.1.6", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", @@ -76,6 +77,7 @@ "safe-buffer": "^5.1.1", "safe-compare": "^1.1.4", "semver": "^7.1.3", + "split2": "^3.2.2", "tar": "^6.0.1", "tar-fs": "^2.0.0", "ws": "^7.2.0", diff --git a/src/node/cli.ts b/src/node/cli.ts index d3afe203..b8272aa5 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -47,6 +47,8 @@ export interface Args extends VsArgs { readonly _: string[] readonly "reuse-window"?: boolean readonly "new-window"?: boolean + + readonly "expose"?: OptionalString } interface Option { @@ -155,6 +157,9 @@ const options: Options> = { locale: { type: "string" }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, + + "expose": { type: OptionalString, description: "Expose via Coder Cloud with the passed name. You'll get a URL" + + "like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub." }, } export const optionDescriptions = (): string[] => { diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts new file mode 100644 index 00000000..082e2d82 --- /dev/null +++ b/src/node/coder-cloud.ts @@ -0,0 +1,30 @@ +import { spawn } from "child_process" +import path from "path" +import { logger } from "@coder/logger" +import split2 from "split2" + +export async function coderCloudExpose(serverName: string): Promise { + const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") + const agent = spawn(coderCloudAgent, ["link", serverName], { + stdio: ["inherit", "inherit", "pipe"], + }) + + agent.stderr.pipe(split2()).on("data", line => { + line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") + logger.info(line) + }) + + return new Promise((res, rej) => { + agent.on("error", rej) + + agent.on("close", code => { + if (code !== 0) { + rej({ + message: `coder cloud agent exited with ${code}`, + }) + return + } + res() + }) + }) +} diff --git a/src/node/entry.ts b/src/node/entry.ts index a416ae99..860d8de7 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -16,6 +16,7 @@ import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" import { ipcMain, wrap } from "./wrapper" +import { coderCloudExpose } from "./coder-cloud" process.on("uncaughtException", (error) => { logger.error(`Uncaught exception: ${error.message}`) @@ -188,6 +189,20 @@ async function entry(): Promise { process.exit(1) }) vscode.on("exit", (code) => process.exit(code || 0)) + } else if (args["expose"]) { + logger.debug("exposing code-server via the coder-cloud agent") + + if (!args["expose"].value) { + logger.error("You must pass a name to expose with coder cloud. See --help") + process.exit(1) + } + + try { + await coderCloudExpose(args["expose"].value) + } catch (err) { + logger.error(err.message) + process.exit(1) + } } else if (process.env.VSCODE_IPC_HOOK_CLI) { const pipeArgs: OpenCommandPipeArgs = { type: "open", diff --git a/yarn.lock b/yarn.lock index 68221a85..6f388626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1107,6 +1107,13 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== +"@types/split2@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@types/split2/-/split2-2.1.6.tgz#b095c9e064853824b22c67993d99b066777402b1" + integrity sha512-ddaFSOMuy2Rp97l6q/LEteQygvTQJuEZ+SRhxFKR0uXGsdbFDqX/QF2xoGcOqLQ8XV91v01SnAv2vpgihNgW/Q== + dependencies: + "@types/node" "*" + "@types/tar-fs@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1" @@ -5996,7 +6003,7 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6621,6 +6628,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"